Merge "Shadow / jarjar kotlinx-metadata-jvm in room-compiler-processing." into androidx-main am: 20d34810f7
Original change: https://android-review.googlesource.com/c/platform/frameworks/support/+/2584530
Change-Id: I0e251cd3f0dad4aed9f7848d5be366cdf4ab49a2
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/activity/activity-compose/samples/build.gradle b/activity/activity-compose/samples/build.gradle
index e76d4bc..3190107 100644
--- a/activity/activity-compose/samples/build.gradle
+++ b/activity/activity-compose/samples/build.gradle
@@ -40,7 +40,7 @@
}
androidx {
- name = "AndroidX Compose UI Activity Integration Samples"
+ name = "Compose UI Activity Integration Samples"
type = LibraryType.SAMPLES
inceptionYear = "2020"
description = "Samples for Compose integration with Activity"
diff --git a/activity/activity-lint/build.gradle b/activity/activity-lint/build.gradle
index 704ee58..554da78 100644
--- a/activity/activity-lint/build.gradle
+++ b/activity/activity-lint/build.gradle
@@ -36,7 +36,7 @@
}
androidx {
- name = "Android Activity Lint Checks"
+ name = "Activity Lint Checks"
type = LibraryType.LINT
inceptionYear = "2020"
description = "Android Activity Lint Checks"
diff --git a/activity/activity/api/current.txt b/activity/activity/api/current.txt
index c7520eb..532bccd 100644
--- a/activity/activity/api/current.txt
+++ b/activity/activity/api/current.txt
@@ -85,7 +85,10 @@
public abstract class OnBackPressedCallback {
ctor public OnBackPressedCallback(boolean enabled);
+ method @MainThread @RequiresApi(34) public void handleOnBackCancelled();
method @MainThread public abstract void handleOnBackPressed();
+ method @MainThread @RequiresApi(34) public void handleOnBackProgressed(android.window.BackEvent backEvent);
+ method @MainThread @RequiresApi(34) public void handleOnBackStarted(android.window.BackEvent backEvent);
method @MainThread public final boolean isEnabled();
method @MainThread public final void remove();
method @MainThread public final void setEnabled(boolean);
@@ -93,10 +96,14 @@
}
public final class OnBackPressedDispatcher {
+ ctor public OnBackPressedDispatcher(Runnable? fallbackOnBackPressed, androidx.core.util.Consumer<java.lang.Boolean>? onHasEnabledCallbacksChanged);
ctor public OnBackPressedDispatcher(optional Runnable? fallbackOnBackPressed);
ctor public OnBackPressedDispatcher();
method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback onBackPressedCallback);
method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner owner, androidx.activity.OnBackPressedCallback onBackPressedCallback);
+ method @MainThread @RequiresApi(34) @VisibleForTesting public void dispatchOnBackCancelled();
+ method @MainThread @RequiresApi(34) @VisibleForTesting public void dispatchOnBackProgressed(android.window.BackEvent backEvent);
+ method @MainThread @RequiresApi(34) @VisibleForTesting public void dispatchOnBackStarted(android.window.BackEvent backEvent);
method @MainThread public boolean hasEnabledCallbacks();
method @MainThread public void onBackPressed();
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher invoker);
diff --git a/activity/activity/api/public_plus_experimental_current.txt b/activity/activity/api/public_plus_experimental_current.txt
index c7520eb..532bccd 100644
--- a/activity/activity/api/public_plus_experimental_current.txt
+++ b/activity/activity/api/public_plus_experimental_current.txt
@@ -85,7 +85,10 @@
public abstract class OnBackPressedCallback {
ctor public OnBackPressedCallback(boolean enabled);
+ method @MainThread @RequiresApi(34) public void handleOnBackCancelled();
method @MainThread public abstract void handleOnBackPressed();
+ method @MainThread @RequiresApi(34) public void handleOnBackProgressed(android.window.BackEvent backEvent);
+ method @MainThread @RequiresApi(34) public void handleOnBackStarted(android.window.BackEvent backEvent);
method @MainThread public final boolean isEnabled();
method @MainThread public final void remove();
method @MainThread public final void setEnabled(boolean);
@@ -93,10 +96,14 @@
}
public final class OnBackPressedDispatcher {
+ ctor public OnBackPressedDispatcher(Runnable? fallbackOnBackPressed, androidx.core.util.Consumer<java.lang.Boolean>? onHasEnabledCallbacksChanged);
ctor public OnBackPressedDispatcher(optional Runnable? fallbackOnBackPressed);
ctor public OnBackPressedDispatcher();
method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback onBackPressedCallback);
method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner owner, androidx.activity.OnBackPressedCallback onBackPressedCallback);
+ method @MainThread @RequiresApi(34) @VisibleForTesting public void dispatchOnBackCancelled();
+ method @MainThread @RequiresApi(34) @VisibleForTesting public void dispatchOnBackProgressed(android.window.BackEvent backEvent);
+ method @MainThread @RequiresApi(34) @VisibleForTesting public void dispatchOnBackStarted(android.window.BackEvent backEvent);
method @MainThread public boolean hasEnabledCallbacks();
method @MainThread public void onBackPressed();
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher invoker);
diff --git a/activity/activity/api/restricted_current.txt b/activity/activity/api/restricted_current.txt
index 9244450..bafda4a 100644
--- a/activity/activity/api/restricted_current.txt
+++ b/activity/activity/api/restricted_current.txt
@@ -84,7 +84,10 @@
public abstract class OnBackPressedCallback {
ctor public OnBackPressedCallback(boolean enabled);
+ method @MainThread @RequiresApi(34) public void handleOnBackCancelled();
method @MainThread public abstract void handleOnBackPressed();
+ method @MainThread @RequiresApi(34) public void handleOnBackProgressed(android.window.BackEvent backEvent);
+ method @MainThread @RequiresApi(34) public void handleOnBackStarted(android.window.BackEvent backEvent);
method @MainThread public final boolean isEnabled();
method @MainThread public final void remove();
method @MainThread public final void setEnabled(boolean);
@@ -92,10 +95,14 @@
}
public final class OnBackPressedDispatcher {
+ ctor public OnBackPressedDispatcher(Runnable? fallbackOnBackPressed, androidx.core.util.Consumer<java.lang.Boolean>? onHasEnabledCallbacksChanged);
ctor public OnBackPressedDispatcher(optional Runnable? fallbackOnBackPressed);
ctor public OnBackPressedDispatcher();
method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback onBackPressedCallback);
method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner owner, androidx.activity.OnBackPressedCallback onBackPressedCallback);
+ method @MainThread @RequiresApi(34) @VisibleForTesting public void dispatchOnBackCancelled();
+ method @MainThread @RequiresApi(34) @VisibleForTesting public void dispatchOnBackProgressed(android.window.BackEvent backEvent);
+ method @MainThread @RequiresApi(34) @VisibleForTesting public void dispatchOnBackStarted(android.window.BackEvent backEvent);
method @MainThread public boolean hasEnabledCallbacks();
method @MainThread public void onBackPressed();
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher invoker);
diff --git a/activity/activity/lint-baseline.xml b/activity/activity/lint-baseline.xml
index d97aee4..08512e7 100644
--- a/activity/activity/lint-baseline.xml
+++ b/activity/activity/lint-baseline.xml
@@ -10,4 +10,13 @@
file="src/main/java/androidx/activity/ComponentActivity.java"/>
</issue>
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" onBackInvokedCallback = if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/activity/OnBackPressedDispatcher.kt"/>
+ </issue>
+
</issues>
diff --git a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt
index 1ff31b7..47717b5 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt
@@ -17,6 +17,8 @@
package androidx.activity
import android.os.Build
+import android.window.BackEvent
+import android.window.BackEvent.EDGE_LEFT
import android.window.OnBackInvokedCallback
import android.window.OnBackInvokedDispatcher
import androidx.annotation.RequiresApi
@@ -180,4 +182,59 @@
assertThat(unregisterCount).isEqualTo(1)
}
+
+ @Test
+ @RequiresApi(34)
+ @SdkSuppress(minSdkVersion = 34)
+ fun testSimpleAnimatedCallback() {
+ var registerCount = 0
+ var unregisterCount = 0
+ val invoker = object : OnBackInvokedDispatcher {
+ override fun registerOnBackInvokedCallback(p0: Int, p1: OnBackInvokedCallback) {
+ registerCount++
+ }
+
+ override fun unregisterOnBackInvokedCallback(p0: OnBackInvokedCallback) {
+ unregisterCount++
+ }
+ }
+
+ val dispatcher = OnBackPressedDispatcher()
+
+ dispatcher.setOnBackInvokedDispatcher(invoker)
+
+ var startedCount = 0
+ var progressedCount = 0
+ var cancelledCount = 0
+ val callback = object : OnBackPressedCallback(true) {
+ override fun handleOnBackStarted(backEvent: BackEvent) {
+ startedCount++
+ }
+
+ override fun handleOnBackProgressed(backEvent: BackEvent) {
+ progressedCount++
+ }
+ override fun handleOnBackPressed() { }
+ override fun handleOnBackCancelled() {
+ cancelledCount++
+ }
+ }
+
+ dispatcher.addCallback(callback)
+
+ assertThat(registerCount).isEqualTo(1)
+
+ dispatcher.dispatchOnBackStarted(BackEvent(0.1F, 0.1F, 0.1F, EDGE_LEFT))
+ assertThat(startedCount).isEqualTo(1)
+
+ dispatcher.dispatchOnBackProgressed(BackEvent(0.1F, 0.1F, 0.1F, EDGE_LEFT))
+ assertThat(progressedCount).isEqualTo(1)
+
+ dispatcher.dispatchOnBackCancelled()
+ assertThat(cancelledCount).isEqualTo(1)
+
+ callback.remove()
+
+ assertThat(unregisterCount).isEqualTo(1)
+ }
}
diff --git a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
index 51cf3de..afbe0ea 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
@@ -441,6 +441,80 @@
withActivity { realDispatcher.onBackPressed() }
}
}
+
+ @Test
+ fun testOnHasEnabledCallbacks() {
+ var reportedHasEnabledCallbacks = false
+ var reportCount = 0
+ val dispatcher = OnBackPressedDispatcher(
+ fallbackOnBackPressed = null,
+ onHasEnabledCallbacksChanged = {
+ reportedHasEnabledCallbacks = it
+ reportCount++
+ }
+ )
+
+ assertWithMessage("initial reportCount")
+ .that(reportCount)
+ .isEqualTo(0)
+ assertWithMessage("initial reportedHasEnabledCallbacks")
+ .that(reportedHasEnabledCallbacks)
+ .isFalse()
+
+ val callbackA = dispatcher.addCallback(enabled = false) {}
+
+ assertWithMessage("reportCount")
+ .that(reportCount)
+ .isEqualTo(0)
+ assertWithMessage("reportedHasEnabledCallbacks")
+ .that(reportedHasEnabledCallbacks)
+ .isFalse()
+
+ callbackA.isEnabled = true
+
+ assertWithMessage("reportCount")
+ .that(reportCount)
+ .isEqualTo(1)
+ assertWithMessage("reportedHasEnabledCallbacks")
+ .that(reportedHasEnabledCallbacks)
+ .isTrue()
+
+ val callbackB = dispatcher.addCallback {}
+
+ assertWithMessage("reportCount")
+ .that(reportCount)
+ .isEqualTo(1)
+ assertWithMessage("reportedHasEnabledCallbacks")
+ .that(reportedHasEnabledCallbacks)
+ .isTrue()
+
+ callbackA.remove()
+
+ assertWithMessage("reportCount")
+ .that(reportCount)
+ .isEqualTo(1)
+ assertWithMessage("reportedHasEnabledCallbacks")
+ .that(reportedHasEnabledCallbacks)
+ .isTrue()
+
+ callbackB.remove()
+
+ assertWithMessage("reportCount")
+ .that(reportCount)
+ .isEqualTo(2)
+ assertWithMessage("reportedHasEnabledCallbacks")
+ .that(reportedHasEnabledCallbacks)
+ .isFalse()
+
+ dispatcher.addCallback {}
+
+ assertWithMessage("reportCount")
+ .that(reportCount)
+ .isEqualTo(3)
+ assertWithMessage("reportedHasEnabledCallbacks")
+ .that(reportedHasEnabledCallbacks)
+ .isTrue()
+ }
}
open class CountingOnBackPressedCallback(
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt b/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt
index 3101ef7..bc4b001 100644
--- a/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt
+++ b/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt
@@ -15,7 +15,9 @@
*/
package androidx.activity
+import android.window.BackEvent
import androidx.annotation.MainThread
+import androidx.annotation.RequiresApi
import java.util.concurrent.CopyOnWriteArrayList
/**
@@ -67,11 +69,38 @@
fun remove() = cancellables.forEach { it.cancel() }
/**
+ * Callback for handling the system UI generated equivalent to
+ * [OnBackPressedDispatcher.dispatchOnBackStarted].
+ */
+ @Suppress("CallbackMethodName") /* mirror handleOnBackPressed local style */
+ @RequiresApi(34)
+ @MainThread
+ open fun handleOnBackStarted(backEvent: BackEvent) {}
+
+ /**
+ * Callback for handling the system UI generated equivalent to
+ * [OnBackPressedDispatcher.dispatchOnBackProgressed].
+ */
+ @Suppress("CallbackMethodName") /* mirror handleOnBackPressed local style */
+ @RequiresApi(34)
+ @MainThread
+ open fun handleOnBackProgressed(backEvent: BackEvent) {}
+
+ /**
* Callback for handling the [OnBackPressedDispatcher.onBackPressed] event.
*/
@MainThread
abstract fun handleOnBackPressed()
+ /**
+ * Callback for handling the system UI generated equivalent to
+ * [OnBackPressedDispatcher.dispatchOnBackCancelled].
+ */
+ @Suppress("CallbackMethodName") /* mirror handleOnBackPressed local style */
+ @RequiresApi(34)
+ @MainThread
+ open fun handleOnBackCancelled() {}
+
@JvmName("addCancellable")
internal fun addCancellable(cancellable: Cancellable) {
cancellables.add(cancellable)
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
index 5319d9e..0c97526 100644
--- a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
+++ b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
@@ -16,11 +16,18 @@
package androidx.activity
import android.os.Build
+import android.window.BackEvent
+import android.window.OnBackAnimationCallback
import android.window.OnBackInvokedCallback
import android.window.OnBackInvokedDispatcher
import androidx.annotation.DoNotInline
import androidx.annotation.MainThread
+import androidx.annotation.OptIn
import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.core.os.BuildCompat
+import androidx.core.os.BuildCompat.PrereleaseSdkCheck
+import androidx.core.util.Consumer
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
@@ -51,14 +58,26 @@
* When constructing an instance of this class, the [fallbackOnBackPressed] can be set to
* receive a callback if [onBackPressed] is called when [hasEnabledCallbacks] returns `false`.
*/
-class OnBackPressedDispatcher @JvmOverloads constructor(
- private val fallbackOnBackPressed: Runnable? = null
+// Implementation/API compatibility note: previous releases included only the Runnable? constructor,
+// which permitted both first-argument and trailing lambda call syntax to specify
+// fallbackOnBackPressed. To avoid silently breaking source compatibility the new
+// primary constructor has no optional parameters to avoid ambiguity/wrong overload resolution
+// when a single parameter is provided as a trailing lambda.
+@OptIn(PrereleaseSdkCheck::class)
+class OnBackPressedDispatcher constructor(
+ private val fallbackOnBackPressed: Runnable?,
+ private val onHasEnabledCallbacksChanged: Consumer<Boolean>?
) {
private val onBackPressedCallbacks = ArrayDeque<OnBackPressedCallback>()
- private var enabledChangedCallback: (() -> Unit)? = null
private var onBackInvokedCallback: OnBackInvokedCallback? = null
private var invokedDispatcher: OnBackInvokedDispatcher? = null
private var backInvokedCallbackRegistered = false
+ private var hasEnabledCallbacks = false
+
+ @JvmOverloads
+ constructor(
+ fallbackOnBackPressed: Runnable? = null
+ ) : this(fallbackOnBackPressed, null)
/**
* Sets the [OnBackInvokedDispatcher] for handling system back for Android SDK T+.
@@ -68,12 +87,11 @@
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun setOnBackInvokedDispatcher(invoker: OnBackInvokedDispatcher) {
invokedDispatcher = invoker
- updateBackInvokedCallbackState()
+ updateBackInvokedCallbackState(hasEnabledCallbacks)
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
- internal fun updateBackInvokedCallbackState() {
- val shouldBeRegistered = hasEnabledCallbacks()
+ private fun updateBackInvokedCallbackState(shouldBeRegistered: Boolean) {
val dispatcher = invokedDispatcher
val onBackInvokedCallback = onBackInvokedCallback
if (dispatcher != null && onBackInvokedCallback != null) {
@@ -94,12 +112,30 @@
}
}
+ private fun updateEnabledCallbacks() {
+ val hadEnabledCallbacks = hasEnabledCallbacks
+ val hasEnabledCallbacks = onBackPressedCallbacks.any { it.isEnabled }
+ this.hasEnabledCallbacks = hasEnabledCallbacks
+ if (hasEnabledCallbacks != hadEnabledCallbacks) {
+ onHasEnabledCallbacksChanged?.accept(hasEnabledCallbacks)
+ if (Build.VERSION.SDK_INT >= 33) {
+ updateBackInvokedCallbackState(hasEnabledCallbacks)
+ }
+ }
+ }
+
init {
if (Build.VERSION.SDK_INT >= 33) {
- enabledChangedCallback = {
- updateBackInvokedCallbackState()
+ onBackInvokedCallback = if (BuildCompat.isAtLeastU()) {
+ Api34Impl.createOnBackAnimationCallback(
+ { backEvent -> onBackStarted(backEvent) },
+ { backEvent -> onBackProgressed(backEvent) },
+ { onBackPressed() },
+ { onBackCancelled() }
+ )
+ } else {
+ Api33Impl.createOnBackInvokedCallback { onBackPressed() }
}
- onBackInvokedCallback = Api33Impl.createOnBackInvokedCallback { onBackPressed() }
}
}
@@ -137,10 +173,8 @@
onBackPressedCallbacks.add(onBackPressedCallback)
val cancellable = OnBackPressedCancellable(onBackPressedCallback)
onBackPressedCallback.addCancellable(cancellable)
- if (Build.VERSION.SDK_INT >= 33) {
- updateBackInvokedCallbackState()
- onBackPressedCallback.enabledChangedCallback = enabledChangedCallback
- }
+ updateEnabledCallbacks()
+ onBackPressedCallback.enabledChangedCallback = ::updateEnabledCallbacks
return cancellable
}
@@ -178,21 +212,55 @@
onBackPressedCallback.addCancellable(
LifecycleOnBackPressedCancellable(lifecycle, onBackPressedCallback)
)
- if (Build.VERSION.SDK_INT >= 33) {
- updateBackInvokedCallbackState()
- onBackPressedCallback.enabledChangedCallback = enabledChangedCallback
- }
+ updateEnabledCallbacks()
+ onBackPressedCallback.enabledChangedCallback = ::updateEnabledCallbacks
}
/**
- * Checks if there is at least one [enabled][OnBackPressedCallback.isEnabled]
+ * Returns `true` if there is at least one [enabled][OnBackPressedCallback.isEnabled]
* callback registered with this dispatcher.
*
* @return True if there is at least one enabled callback.
*/
@MainThread
- fun hasEnabledCallbacks(): Boolean = onBackPressedCallbacks.any {
- it.isEnabled
+ fun hasEnabledCallbacks(): Boolean = hasEnabledCallbacks
+
+ @VisibleForTesting
+ @RequiresApi(34)
+ @MainThread
+ fun dispatchOnBackStarted(backEvent: BackEvent) {
+ onBackStarted(backEvent)
+ }
+
+ @RequiresApi(34)
+ @MainThread
+ private fun onBackStarted(backEvent: BackEvent) {
+ val callback = onBackPressedCallbacks.lastOrNull {
+ it.isEnabled
+ }
+ if (callback != null) {
+ callback.handleOnBackStarted(backEvent)
+ return
+ }
+ }
+
+ @VisibleForTesting
+ @RequiresApi(34)
+ @MainThread
+ fun dispatchOnBackProgressed(backEvent: BackEvent) {
+ onBackProgressed(backEvent)
+ }
+
+ @RequiresApi(34)
+ @MainThread
+ private fun onBackProgressed(backEvent: BackEvent) {
+ val callback = onBackPressedCallbacks.lastOrNull {
+ it.isEnabled
+ }
+ if (callback != null) {
+ callback.handleOnBackProgressed(backEvent)
+ return
+ }
}
/**
@@ -216,16 +284,33 @@
fallbackOnBackPressed?.run()
}
+ @VisibleForTesting
+ @RequiresApi(34)
+ @MainThread
+ fun dispatchOnBackCancelled() {
+ onBackCancelled()
+ }
+
+ @RequiresApi(34)
+ @MainThread
+ private fun onBackCancelled() {
+ val callback = onBackPressedCallbacks.lastOrNull {
+ it.isEnabled
+ }
+ if (callback != null) {
+ callback.handleOnBackCancelled()
+ return
+ }
+ }
+
private inner class OnBackPressedCancellable(
private val onBackPressedCallback: OnBackPressedCallback
) : Cancellable {
override fun cancel() {
onBackPressedCallbacks.remove(onBackPressedCallback)
onBackPressedCallback.removeCancellable(this)
- if (Build.VERSION.SDK_INT >= 33) {
- onBackPressedCallback.enabledChangedCallback = null
- updateBackInvokedCallbackState()
- }
+ onBackPressedCallback.enabledChangedCallback?.invoke()
+ onBackPressedCallback.enabledChangedCallback = null
}
}
@@ -286,6 +371,35 @@
return OnBackInvokedCallback { onBackInvoked() }
}
}
+
+ @RequiresApi(34)
+ internal object Api34Impl {
+ @DoNotInline
+ fun createOnBackAnimationCallback(
+ onBackStarted: (backEvent: BackEvent) -> Unit,
+ onBackProgressed: (backEvent: BackEvent) -> Unit,
+ onBackInvoked: () -> Unit,
+ onBackCancelled: () -> Unit
+ ): OnBackInvokedCallback {
+ return object : OnBackAnimationCallback {
+ override fun onBackStarted(backEvent: BackEvent) {
+ onBackStarted(backEvent)
+ }
+
+ override fun onBackProgressed(backEvent: BackEvent) {
+ onBackProgressed(backEvent)
+ }
+
+ override fun onBackInvoked() {
+ onBackInvoked()
+ }
+
+ override fun onBackCancelled() {
+ onBackCancelled()
+ }
+ }
+ }
+ }
}
/**
diff --git a/annotation/annotation/build.gradle b/annotation/annotation/build.gradle
index e821397..769ac65 100644
--- a/annotation/annotation/build.gradle
+++ b/annotation/annotation/build.gradle
@@ -66,10 +66,10 @@
}
androidx {
- name = "Android Support Library Annotations"
+ name = "Annotation"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.ANNOTATION
mavenMultiplatformVersion = LibraryVersions.ANNOTATION_KMP
inceptionYear = "2013"
- description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs."
+ description = "Provides source annotations for tooling and readability."
}
diff --git a/appactions/builtintypes/builtintypes-core/build.gradle b/appactions/builtintypes/builtintypes-core/build.gradle
index 9d98eb3..828e4de 100644
--- a/appactions/builtintypes/builtintypes-core/build.gradle
+++ b/appactions/builtintypes/builtintypes-core/build.gradle
@@ -43,7 +43,7 @@
}
androidx {
- name = "androidx.appactions.builtintypes:builtintypes-core"
+ name = "AppActions Builtin Types Core"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2023"
description = "This library exposes a core set of data types based on schema.org definitions."
diff --git a/appactions/interaction/interaction-service/src/test/resources/robolectric.properties b/appactions/interaction/interaction-service/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..69fde47
--- /dev/null
+++ b/appactions/interaction/interaction-service/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/appcompat/appcompat-resources/api/api_lint.ignore b/appcompat/appcompat-resources/api/api_lint.ignore
index 0cfa261..dd0cf8d 100644
--- a/appcompat/appcompat-resources/api/api_lint.ignore
+++ b/appcompat/appcompat-resources/api/api_lint.ignore
@@ -17,6 +17,8 @@
Missing nullability on parameter `tint` in method `setTintList`
MissingNullability: androidx.appcompat.graphics.drawable.DrawableWrapperCompat#DrawableWrapperCompat(android.graphics.drawable.Drawable) parameter #0:
Missing nullability on parameter `drawable` in method `DrawableWrapperCompat`
+MissingNullability: androidx.appcompat.graphics.drawable.DrawableWrapperCompat#draw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `draw`
MissingNullability: androidx.appcompat.graphics.drawable.DrawableWrapperCompat#getCurrent():
Missing nullability on method `getCurrent` return
MissingNullability: androidx.appcompat.graphics.drawable.DrawableWrapperCompat#getPadding(android.graphics.Rect) parameter #0:
diff --git a/appcompat/appcompat-resources/build.gradle b/appcompat/appcompat-resources/build.gradle
index cd96606..7686cb1 100644
--- a/appcompat/appcompat-resources/build.gradle
+++ b/appcompat/appcompat-resources/build.gradle
@@ -59,8 +59,9 @@
}
androidx {
- name = "Android Resources Library"
+ name = "AppCompat Resources"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
- description = "The Resources Library is a static library that you can add to your Android application in order to use resource APIs that backport the latest APIs to older versions of the platform. Compatible on devices running API 14 or later."
+ description = "Provides backward-compatible implementations of resource-related Android SDK" +
+ "functionality, including color state list theming."
}
diff --git a/appcompat/appcompat/api/api_lint.ignore b/appcompat/appcompat/api/api_lint.ignore
index ff1d34f..90541cfd 100644
--- a/appcompat/appcompat/api/api_lint.ignore
+++ b/appcompat/appcompat/api/api_lint.ignore
@@ -91,12 +91,8 @@
Invalid nullability on parameter `filters` in method `setFilters`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
InvalidNullabilityOverride: androidx.appcompat.widget.AppCompatToggleButton#setFilters(android.text.InputFilter[]) parameter #0:
Invalid nullability on parameter `filters` in method `setFilters`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.appcompat.widget.LinearLayoutCompat#onDraw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `canvas` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
InvalidNullabilityOverride: androidx.appcompat.widget.ListPopupWindow#getListView():
Invalid nullability on method `getListView` return. Overrides of unannotated super method cannot be Nullable.
-InvalidNullabilityOverride: androidx.appcompat.widget.SwitchCompat#draw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `c` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
InvalidNullabilityOverride: androidx.appcompat.widget.SwitchCompat#getCustomSelectionActionModeCallback():
Invalid nullability on method `getCustomSelectionActionModeCallback` return. Overrides of unannotated super method cannot be Nullable.
InvalidNullabilityOverride: androidx.appcompat.widget.SwitchCompat#setFilters(android.text.InputFilter[]) parameter #0:
@@ -555,6 +551,8 @@
Missing nullability on parameter `attrs` in method `createView`
MissingNullability: androidx.appcompat.graphics.drawable.DrawerArrowDrawable#DrawerArrowDrawable(android.content.Context) parameter #0:
Missing nullability on parameter `context` in method `DrawerArrowDrawable`
+MissingNullability: androidx.appcompat.graphics.drawable.DrawerArrowDrawable#draw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `draw`
MissingNullability: androidx.appcompat.graphics.drawable.DrawerArrowDrawable#getPaint():
Missing nullability on method `getPaint` return
MissingNullability: androidx.appcompat.graphics.drawable.DrawerArrowDrawable#setColorFilter(android.graphics.ColorFilter) parameter #0:
@@ -727,6 +725,8 @@
Missing nullability on parameter `p` in method `generateLayoutParams`
MissingNullability: androidx.appcompat.widget.LinearLayoutCompat#getDividerDrawable():
Missing nullability on method `getDividerDrawable` return
+MissingNullability: androidx.appcompat.widget.LinearLayoutCompat#onDraw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `onDraw`
MissingNullability: androidx.appcompat.widget.LinearLayoutCompat#onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent) parameter #0:
Missing nullability on parameter `event` in method `onInitializeAccessibilityEvent`
MissingNullability: androidx.appcompat.widget.LinearLayoutCompat#onInitializeAccessibilityNodeInfo(android.view.accessibility.AccessibilityNodeInfo) parameter #0:
@@ -797,6 +797,8 @@
Missing nullability on parameter `source` in method `onShareTargetSelected`
MissingNullability: androidx.appcompat.widget.ShareActionProvider.OnShareTargetSelectedListener#onShareTargetSelected(androidx.appcompat.widget.ShareActionProvider, android.content.Intent) parameter #1:
Missing nullability on parameter `intent` in method `onShareTargetSelected`
+MissingNullability: androidx.appcompat.widget.SwitchCompat#draw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `c` in method `draw`
MissingNullability: androidx.appcompat.widget.SwitchCompat#getTextOff():
Missing nullability on method `getTextOff` return
MissingNullability: androidx.appcompat.widget.SwitchCompat#getTextOn():
@@ -831,6 +833,8 @@
Missing nullability on parameter `thumb` in method `setThumbDrawable`
MissingNullability: androidx.appcompat.widget.SwitchCompat#setTrackDrawable(android.graphics.drawable.Drawable) parameter #0:
Missing nullability on parameter `track` in method `setTrackDrawable`
+MissingNullability: androidx.appcompat.widget.SwitchCompat#verifyDrawable(android.graphics.drawable.Drawable) parameter #0:
+ Missing nullability on parameter `who` in method `verifyDrawable`
MissingNullability: androidx.appcompat.widget.Toolbar#checkLayoutParams(android.view.ViewGroup.LayoutParams) parameter #0:
Missing nullability on parameter `p` in method `checkLayoutParams`
MissingNullability: androidx.appcompat.widget.Toolbar#generateDefaultLayoutParams():
diff --git a/appcompat/appcompat/build.gradle b/appcompat/appcompat/build.gradle
index 614c612..22296ee 100644
--- a/appcompat/appcompat/build.gradle
+++ b/appcompat/appcompat/build.gradle
@@ -104,9 +104,10 @@
}
androidx {
- name = "Android AppCompat Library"
+ name = "AppCompat"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2011"
- description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\'t a part of the framework APIs. Compatible on devices running API 14 or later."
+ description = "Provides backwards-compatible implementations of UI-related Android SDK " +
+ "functionality, including dark mode and Material theming."
failOnDeprecationWarnings = false
}
diff --git a/appcompat/appcompat/lint-baseline.xml b/appcompat/appcompat/lint-baseline.xml
index dc8d901..61c19d7 100644
--- a/appcompat/appcompat/lint-baseline.xml
+++ b/appcompat/appcompat/lint-baseline.xml
@@ -3448,6 +3448,15 @@
<issue
id="UnknownNullness"
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected synchronized void onDraw(Canvas canvas) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/appcompat/widget/AppCompatSeekBar.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
errorLine1=" int defStyleAttr, int mode, Resources.Theme popupTheme) {"
errorLine2=" ~~~~~~~~~~~~~~~">
<location
@@ -7111,6 +7120,15 @@
<issue
id="UnknownNullness"
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void onDraw(Canvas canvas) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/appcompat/widget/SwitchCompat.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
errorLine1=" public static TintTypedArray obtainStyledAttributes(Context context, AttributeSet set,"
errorLine2=" ~~~~~~~~~~~~~~">
<location
@@ -7966,6 +7984,15 @@
<issue
id="UnknownNullness"
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void dispatchDraw(Canvas canvas) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/appcompat/widget/ViewStubCompat.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
errorLine1=" public View inflate() {"
errorLine2=" ~~~~">
<location
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java
index 069c76c..5719451 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,4 +20,3 @@
* An activity for locales with a unique class name.
*/
public class LocalesActivityA extends LocalesUpdateActivity {}
-
diff --git a/appintegration/OWNERS b/appintegration/OWNERS
new file mode 100644
index 0000000..c29ded6
--- /dev/null
+++ b/appintegration/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 1293429
+yim@google.com
diff --git a/appsearch/appsearch-builtin-types/api/current.txt b/appsearch/appsearch-builtin-types/api/current.txt
index cb472ce..ffae60c 100644
--- a/appsearch/appsearch-builtin-types/api/current.txt
+++ b/appsearch/appsearch-builtin-types/api/current.txt
@@ -29,8 +29,10 @@
ctor public Alarm.Builder(String, String);
ctor public Alarm.Builder(androidx.appsearch.builtintypes.Alarm);
method public androidx.appsearch.builtintypes.Alarm.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.Alarm.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.Alarm build();
method public androidx.appsearch.builtintypes.Alarm.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.Alarm.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutPeriodEndDate(String?);
method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutPeriodStartDate(String?);
method public androidx.appsearch.builtintypes.Alarm.Builder setCreationTimestampMillis(long);
@@ -66,8 +68,10 @@
ctor public AlarmInstance.Builder(String, String, String);
ctor public AlarmInstance.Builder(androidx.appsearch.builtintypes.AlarmInstance);
method public androidx.appsearch.builtintypes.AlarmInstance.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.AlarmInstance build();
method public androidx.appsearch.builtintypes.AlarmInstance.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.AlarmInstance.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.AlarmInstance.Builder setDescription(String?);
method public androidx.appsearch.builtintypes.AlarmInstance.Builder setDocumentScore(int);
@@ -90,8 +94,10 @@
ctor public ContactPoint.Builder(String, String, String);
ctor public ContactPoint.Builder(androidx.appsearch.builtintypes.ContactPoint);
method public androidx.appsearch.builtintypes.ContactPoint.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.ContactPoint.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.ContactPoint build();
method public androidx.appsearch.builtintypes.ContactPoint.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.ContactPoint.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.ContactPoint.Builder setAddresses(java.util.List<java.lang.String!>);
method public androidx.appsearch.builtintypes.ContactPoint.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.ContactPoint.Builder setDescription(String?);
@@ -117,8 +123,10 @@
method public androidx.appsearch.builtintypes.ImageObject.Builder addKeyword(String);
method public androidx.appsearch.builtintypes.ImageObject.Builder addKeyword(androidx.appsearch.builtintypes.properties.Keyword);
method public androidx.appsearch.builtintypes.ImageObject.Builder addKeywords(Iterable<androidx.appsearch.builtintypes.properties.Keyword!>);
+ method public androidx.appsearch.builtintypes.ImageObject.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.ImageObject build();
method public androidx.appsearch.builtintypes.ImageObject.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.ImageObject.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.ImageObject.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.ImageObject.Builder setDescription(String?);
method public androidx.appsearch.builtintypes.ImageObject.Builder setDocumentScore(int);
@@ -159,8 +167,10 @@
ctor public Person.Builder(String, String, String);
ctor public Person.Builder(androidx.appsearch.builtintypes.Person);
method public androidx.appsearch.builtintypes.Person.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.Person.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.Person build();
method public androidx.appsearch.builtintypes.Person.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.Person.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.Person.Builder setAdditionalNames(java.util.List<androidx.appsearch.builtintypes.Person.AdditionalName!>);
method public androidx.appsearch.builtintypes.Person.Builder setAffiliations(java.util.List<java.lang.String!>);
method public androidx.appsearch.builtintypes.Person.Builder setBot(boolean);
@@ -182,6 +192,21 @@
method public androidx.appsearch.builtintypes.Person.Builder setUrl(String?);
}
+ @androidx.appsearch.annotation.Document(name="builtin:PotentialAction") public class PotentialAction {
+ method public String? getDescription();
+ method public String? getName();
+ method public String? getUri();
+ }
+
+ public static final class PotentialAction.Builder {
+ ctor public PotentialAction.Builder();
+ ctor public PotentialAction.Builder(androidx.appsearch.builtintypes.PotentialAction);
+ method public androidx.appsearch.builtintypes.PotentialAction build();
+ method public androidx.appsearch.builtintypes.PotentialAction.Builder setDescription(String?);
+ method public androidx.appsearch.builtintypes.PotentialAction.Builder setName(String?);
+ method public androidx.appsearch.builtintypes.PotentialAction.Builder setUri(String?);
+ }
+
@androidx.appsearch.annotation.Document(name="builtin:Stopwatch") public class Stopwatch extends androidx.appsearch.builtintypes.Thing {
method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) public long calculateBaseTimeMillis(android.content.Context);
method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) public long calculateCurrentAccumulatedDurationMillis(android.content.Context);
@@ -201,8 +226,10 @@
ctor public Stopwatch.Builder(String, String);
ctor public Stopwatch.Builder(androidx.appsearch.builtintypes.Stopwatch);
method public androidx.appsearch.builtintypes.Stopwatch.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.Stopwatch.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.Stopwatch build();
method public androidx.appsearch.builtintypes.Stopwatch.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.Stopwatch.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.Stopwatch.Builder setAccumulatedDurationMillis(long);
method public androidx.appsearch.builtintypes.Stopwatch.Builder setBaseTimeMillis(long, long, int);
method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) public androidx.appsearch.builtintypes.Stopwatch.Builder setBaseTimeMillis(android.content.Context, long, long);
@@ -227,8 +254,10 @@
ctor public StopwatchLap.Builder(String, String);
ctor public StopwatchLap.Builder(androidx.appsearch.builtintypes.StopwatchLap);
method public androidx.appsearch.builtintypes.StopwatchLap.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.StopwatchLap.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.StopwatchLap build();
method public androidx.appsearch.builtintypes.StopwatchLap.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.StopwatchLap.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.StopwatchLap.Builder setAccumulatedLapDurationMillis(long);
method public androidx.appsearch.builtintypes.StopwatchLap.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.StopwatchLap.Builder setDescription(String?);
@@ -251,6 +280,7 @@
method public String? getImage();
method public String? getName();
method public String getNamespace();
+ method public java.util.List<androidx.appsearch.builtintypes.PotentialAction!> getPotentialActions();
method public String? getUrl();
}
@@ -258,8 +288,10 @@
ctor public Thing.Builder(String, String);
ctor public Thing.Builder(androidx.appsearch.builtintypes.Thing);
method public androidx.appsearch.builtintypes.Thing.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.Thing.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.Thing build();
method public androidx.appsearch.builtintypes.Thing.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.Thing.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.Thing.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.Thing.Builder setDescription(String?);
method public androidx.appsearch.builtintypes.Thing.Builder setDocumentScore(int);
@@ -295,8 +327,10 @@
ctor public Timer.Builder(String, String);
ctor public Timer.Builder(androidx.appsearch.builtintypes.Timer);
method public androidx.appsearch.builtintypes.Timer.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.Timer.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.Timer build();
method public androidx.appsearch.builtintypes.Timer.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.Timer.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.Timer.Builder setBaseTimeMillis(long, long, int);
method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) public androidx.appsearch.builtintypes.Timer.Builder setBaseTimeMillis(android.content.Context, long, long);
method public androidx.appsearch.builtintypes.Timer.Builder setCreationTimestampMillis(long);
diff --git a/appsearch/appsearch-builtin-types/api/public_plus_experimental_current.txt b/appsearch/appsearch-builtin-types/api/public_plus_experimental_current.txt
index cb472ce..ffae60c 100644
--- a/appsearch/appsearch-builtin-types/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch-builtin-types/api/public_plus_experimental_current.txt
@@ -29,8 +29,10 @@
ctor public Alarm.Builder(String, String);
ctor public Alarm.Builder(androidx.appsearch.builtintypes.Alarm);
method public androidx.appsearch.builtintypes.Alarm.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.Alarm.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.Alarm build();
method public androidx.appsearch.builtintypes.Alarm.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.Alarm.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutPeriodEndDate(String?);
method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutPeriodStartDate(String?);
method public androidx.appsearch.builtintypes.Alarm.Builder setCreationTimestampMillis(long);
@@ -66,8 +68,10 @@
ctor public AlarmInstance.Builder(String, String, String);
ctor public AlarmInstance.Builder(androidx.appsearch.builtintypes.AlarmInstance);
method public androidx.appsearch.builtintypes.AlarmInstance.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.AlarmInstance build();
method public androidx.appsearch.builtintypes.AlarmInstance.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.AlarmInstance.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.AlarmInstance.Builder setDescription(String?);
method public androidx.appsearch.builtintypes.AlarmInstance.Builder setDocumentScore(int);
@@ -90,8 +94,10 @@
ctor public ContactPoint.Builder(String, String, String);
ctor public ContactPoint.Builder(androidx.appsearch.builtintypes.ContactPoint);
method public androidx.appsearch.builtintypes.ContactPoint.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.ContactPoint.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.ContactPoint build();
method public androidx.appsearch.builtintypes.ContactPoint.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.ContactPoint.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.ContactPoint.Builder setAddresses(java.util.List<java.lang.String!>);
method public androidx.appsearch.builtintypes.ContactPoint.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.ContactPoint.Builder setDescription(String?);
@@ -117,8 +123,10 @@
method public androidx.appsearch.builtintypes.ImageObject.Builder addKeyword(String);
method public androidx.appsearch.builtintypes.ImageObject.Builder addKeyword(androidx.appsearch.builtintypes.properties.Keyword);
method public androidx.appsearch.builtintypes.ImageObject.Builder addKeywords(Iterable<androidx.appsearch.builtintypes.properties.Keyword!>);
+ method public androidx.appsearch.builtintypes.ImageObject.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.ImageObject build();
method public androidx.appsearch.builtintypes.ImageObject.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.ImageObject.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.ImageObject.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.ImageObject.Builder setDescription(String?);
method public androidx.appsearch.builtintypes.ImageObject.Builder setDocumentScore(int);
@@ -159,8 +167,10 @@
ctor public Person.Builder(String, String, String);
ctor public Person.Builder(androidx.appsearch.builtintypes.Person);
method public androidx.appsearch.builtintypes.Person.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.Person.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.Person build();
method public androidx.appsearch.builtintypes.Person.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.Person.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.Person.Builder setAdditionalNames(java.util.List<androidx.appsearch.builtintypes.Person.AdditionalName!>);
method public androidx.appsearch.builtintypes.Person.Builder setAffiliations(java.util.List<java.lang.String!>);
method public androidx.appsearch.builtintypes.Person.Builder setBot(boolean);
@@ -182,6 +192,21 @@
method public androidx.appsearch.builtintypes.Person.Builder setUrl(String?);
}
+ @androidx.appsearch.annotation.Document(name="builtin:PotentialAction") public class PotentialAction {
+ method public String? getDescription();
+ method public String? getName();
+ method public String? getUri();
+ }
+
+ public static final class PotentialAction.Builder {
+ ctor public PotentialAction.Builder();
+ ctor public PotentialAction.Builder(androidx.appsearch.builtintypes.PotentialAction);
+ method public androidx.appsearch.builtintypes.PotentialAction build();
+ method public androidx.appsearch.builtintypes.PotentialAction.Builder setDescription(String?);
+ method public androidx.appsearch.builtintypes.PotentialAction.Builder setName(String?);
+ method public androidx.appsearch.builtintypes.PotentialAction.Builder setUri(String?);
+ }
+
@androidx.appsearch.annotation.Document(name="builtin:Stopwatch") public class Stopwatch extends androidx.appsearch.builtintypes.Thing {
method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) public long calculateBaseTimeMillis(android.content.Context);
method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) public long calculateCurrentAccumulatedDurationMillis(android.content.Context);
@@ -201,8 +226,10 @@
ctor public Stopwatch.Builder(String, String);
ctor public Stopwatch.Builder(androidx.appsearch.builtintypes.Stopwatch);
method public androidx.appsearch.builtintypes.Stopwatch.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.Stopwatch.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.Stopwatch build();
method public androidx.appsearch.builtintypes.Stopwatch.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.Stopwatch.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.Stopwatch.Builder setAccumulatedDurationMillis(long);
method public androidx.appsearch.builtintypes.Stopwatch.Builder setBaseTimeMillis(long, long, int);
method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) public androidx.appsearch.builtintypes.Stopwatch.Builder setBaseTimeMillis(android.content.Context, long, long);
@@ -227,8 +254,10 @@
ctor public StopwatchLap.Builder(String, String);
ctor public StopwatchLap.Builder(androidx.appsearch.builtintypes.StopwatchLap);
method public androidx.appsearch.builtintypes.StopwatchLap.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.StopwatchLap.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.StopwatchLap build();
method public androidx.appsearch.builtintypes.StopwatchLap.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.StopwatchLap.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.StopwatchLap.Builder setAccumulatedLapDurationMillis(long);
method public androidx.appsearch.builtintypes.StopwatchLap.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.StopwatchLap.Builder setDescription(String?);
@@ -251,6 +280,7 @@
method public String? getImage();
method public String? getName();
method public String getNamespace();
+ method public java.util.List<androidx.appsearch.builtintypes.PotentialAction!> getPotentialActions();
method public String? getUrl();
}
@@ -258,8 +288,10 @@
ctor public Thing.Builder(String, String);
ctor public Thing.Builder(androidx.appsearch.builtintypes.Thing);
method public androidx.appsearch.builtintypes.Thing.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.Thing.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.Thing build();
method public androidx.appsearch.builtintypes.Thing.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.Thing.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.Thing.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.Thing.Builder setDescription(String?);
method public androidx.appsearch.builtintypes.Thing.Builder setDocumentScore(int);
@@ -295,8 +327,10 @@
ctor public Timer.Builder(String, String);
ctor public Timer.Builder(androidx.appsearch.builtintypes.Timer);
method public androidx.appsearch.builtintypes.Timer.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.Timer.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.Timer build();
method public androidx.appsearch.builtintypes.Timer.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.Timer.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.Timer.Builder setBaseTimeMillis(long, long, int);
method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) public androidx.appsearch.builtintypes.Timer.Builder setBaseTimeMillis(android.content.Context, long, long);
method public androidx.appsearch.builtintypes.Timer.Builder setCreationTimestampMillis(long);
diff --git a/appsearch/appsearch-builtin-types/api/restricted_current.txt b/appsearch/appsearch-builtin-types/api/restricted_current.txt
index 0123823..84dbecb 100644
--- a/appsearch/appsearch-builtin-types/api/restricted_current.txt
+++ b/appsearch/appsearch-builtin-types/api/restricted_current.txt
@@ -31,8 +31,10 @@
ctor public Alarm.Builder(String, String);
ctor public Alarm.Builder(androidx.appsearch.builtintypes.Alarm);
method public androidx.appsearch.builtintypes.Alarm.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.Alarm.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.Alarm build();
method public androidx.appsearch.builtintypes.Alarm.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.Alarm.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutPeriodEndDate(String?);
method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutPeriodStartDate(String?);
method public androidx.appsearch.builtintypes.Alarm.Builder setCreationTimestampMillis(long);
@@ -68,8 +70,10 @@
ctor public AlarmInstance.Builder(String, String, String);
ctor public AlarmInstance.Builder(androidx.appsearch.builtintypes.AlarmInstance);
method public androidx.appsearch.builtintypes.AlarmInstance.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.AlarmInstance build();
method public androidx.appsearch.builtintypes.AlarmInstance.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.AlarmInstance.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.AlarmInstance.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.AlarmInstance.Builder setDescription(String?);
method public androidx.appsearch.builtintypes.AlarmInstance.Builder setDocumentScore(int);
@@ -92,8 +96,10 @@
ctor public ContactPoint.Builder(String, String, String);
ctor public ContactPoint.Builder(androidx.appsearch.builtintypes.ContactPoint);
method public androidx.appsearch.builtintypes.ContactPoint.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.ContactPoint.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.ContactPoint build();
method public androidx.appsearch.builtintypes.ContactPoint.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.ContactPoint.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.ContactPoint.Builder setAddresses(java.util.List<java.lang.String!>);
method public androidx.appsearch.builtintypes.ContactPoint.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.ContactPoint.Builder setDescription(String?);
@@ -119,8 +125,10 @@
method public androidx.appsearch.builtintypes.ImageObject.Builder addKeyword(String);
method public androidx.appsearch.builtintypes.ImageObject.Builder addKeyword(androidx.appsearch.builtintypes.properties.Keyword);
method public androidx.appsearch.builtintypes.ImageObject.Builder addKeywords(Iterable<androidx.appsearch.builtintypes.properties.Keyword!>);
+ method public androidx.appsearch.builtintypes.ImageObject.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.ImageObject build();
method public androidx.appsearch.builtintypes.ImageObject.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.ImageObject.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.ImageObject.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.ImageObject.Builder setDescription(String?);
method public androidx.appsearch.builtintypes.ImageObject.Builder setDocumentScore(int);
@@ -161,8 +169,10 @@
ctor public Person.Builder(String, String, String);
ctor public Person.Builder(androidx.appsearch.builtintypes.Person);
method public androidx.appsearch.builtintypes.Person.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.Person.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.Person build();
method public androidx.appsearch.builtintypes.Person.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.Person.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.Person.Builder setAdditionalNames(java.util.List<androidx.appsearch.builtintypes.Person.AdditionalName!>);
method public androidx.appsearch.builtintypes.Person.Builder setAffiliations(java.util.List<java.lang.String!>);
method public androidx.appsearch.builtintypes.Person.Builder setBot(boolean);
@@ -184,6 +194,21 @@
method public androidx.appsearch.builtintypes.Person.Builder setUrl(String?);
}
+ @androidx.appsearch.annotation.Document(name="builtin:PotentialAction") public class PotentialAction {
+ method public String? getDescription();
+ method public String? getName();
+ method public String? getUri();
+ }
+
+ public static final class PotentialAction.Builder {
+ ctor public PotentialAction.Builder();
+ ctor public PotentialAction.Builder(androidx.appsearch.builtintypes.PotentialAction);
+ method public androidx.appsearch.builtintypes.PotentialAction build();
+ method public androidx.appsearch.builtintypes.PotentialAction.Builder setDescription(String?);
+ method public androidx.appsearch.builtintypes.PotentialAction.Builder setName(String?);
+ method public androidx.appsearch.builtintypes.PotentialAction.Builder setUri(String?);
+ }
+
@androidx.appsearch.annotation.Document(name="builtin:Stopwatch") public class Stopwatch extends androidx.appsearch.builtintypes.Thing {
method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) public long calculateBaseTimeMillis(android.content.Context);
method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) public long calculateCurrentAccumulatedDurationMillis(android.content.Context);
@@ -203,8 +228,10 @@
ctor public Stopwatch.Builder(String, String);
ctor public Stopwatch.Builder(androidx.appsearch.builtintypes.Stopwatch);
method public androidx.appsearch.builtintypes.Stopwatch.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.Stopwatch.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.Stopwatch build();
method public androidx.appsearch.builtintypes.Stopwatch.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.Stopwatch.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.Stopwatch.Builder setAccumulatedDurationMillis(long);
method public androidx.appsearch.builtintypes.Stopwatch.Builder setBaseTimeMillis(long, long, int);
method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) public androidx.appsearch.builtintypes.Stopwatch.Builder setBaseTimeMillis(android.content.Context, long, long);
@@ -229,8 +256,10 @@
ctor public StopwatchLap.Builder(String, String);
ctor public StopwatchLap.Builder(androidx.appsearch.builtintypes.StopwatchLap);
method public androidx.appsearch.builtintypes.StopwatchLap.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.StopwatchLap.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.StopwatchLap build();
method public androidx.appsearch.builtintypes.StopwatchLap.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.StopwatchLap.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.StopwatchLap.Builder setAccumulatedLapDurationMillis(long);
method public androidx.appsearch.builtintypes.StopwatchLap.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.StopwatchLap.Builder setDescription(String?);
@@ -253,6 +282,7 @@
method public String? getImage();
method public String? getName();
method public String getNamespace();
+ method public java.util.List<androidx.appsearch.builtintypes.PotentialAction!> getPotentialActions();
method public String? getUrl();
}
@@ -260,8 +290,10 @@
ctor public Thing.Builder(String, String);
ctor public Thing.Builder(androidx.appsearch.builtintypes.Thing);
method public androidx.appsearch.builtintypes.Thing.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.Thing.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.Thing build();
method public androidx.appsearch.builtintypes.Thing.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.Thing.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.Thing.Builder setCreationTimestampMillis(long);
method public androidx.appsearch.builtintypes.Thing.Builder setDescription(String?);
method public androidx.appsearch.builtintypes.Thing.Builder setDocumentScore(int);
@@ -297,8 +329,10 @@
ctor public Timer.Builder(String, String);
ctor public Timer.Builder(androidx.appsearch.builtintypes.Timer);
method public androidx.appsearch.builtintypes.Timer.Builder addAlternateName(String);
+ method public androidx.appsearch.builtintypes.Timer.Builder addPotentialAction(androidx.appsearch.builtintypes.PotentialAction);
method public androidx.appsearch.builtintypes.Timer build();
method public androidx.appsearch.builtintypes.Timer.Builder clearAlternateNames();
+ method public androidx.appsearch.builtintypes.Timer.Builder clearPotentialActions();
method public androidx.appsearch.builtintypes.Timer.Builder setBaseTimeMillis(long, long, int);
method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) public androidx.appsearch.builtintypes.Timer.Builder setBaseTimeMillis(android.content.Context, long, long);
method public androidx.appsearch.builtintypes.Timer.Builder setCreationTimestampMillis(long);
diff --git a/appsearch/appsearch-builtin-types/build.gradle b/appsearch/appsearch-builtin-types/build.gradle
index 892b427..369bdb7 100644
--- a/appsearch/appsearch-builtin-types/build.gradle
+++ b/appsearch/appsearch-builtin-types/build.gradle
@@ -41,7 +41,7 @@
}
androidx {
- name = 'AppSearch Builtin Types'
+ name = "AppSearch Builtin Types"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = '2021'
description = 'Contains AppSearch Document classes and builders for a variety of common ' +
diff --git a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/PotentialActionTest.java b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/PotentialActionTest.java
new file mode 100644
index 0000000..a0117d7
--- /dev/null
+++ b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/PotentialActionTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.builtintypes;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.GenericDocument;
+
+import org.junit.Test;
+
+public class PotentialActionTest {
+ @Test
+ public void testBuilder() {
+ PotentialAction potentialAction = new PotentialAction.Builder()
+ .setName("actions.intent.CREATE_CALL")
+ .setDescription("Call John")
+ .build();
+
+ assertThat(potentialAction.getName()).isEqualTo("actions.intent.CREATE_CALL");
+ assertThat(potentialAction.getDescription()).isEqualTo("Call John");
+ }
+
+ @Test
+ public void testBuilderCopy_returnsActionWithAllFieldsCopied() {
+ PotentialAction potentialAction1 = new PotentialAction.Builder()
+ .setName("actions.intent.CREATE_CALL")
+ .setDescription("Call John")
+ .build();
+
+ PotentialAction potentialAction2 = new PotentialAction.Builder(potentialAction1).build();
+ assertThat(potentialAction1.getName()).isEqualTo(potentialAction2.getName());
+ assertThat(potentialAction1.getDescription()).isEqualTo(potentialAction2.getDescription());
+ assertThat(potentialAction1.getUri()).isEqualTo(potentialAction2.getUri());
+ }
+
+ @Test
+ public void testActionToGenericDocument() throws Exception {
+ PotentialAction potentialAction = new PotentialAction.Builder()
+ .setName("actions.intent.CREATE_CALL")
+ .setDescription("Call John")
+ .setUri("tel:555-123-4567")
+ .build();
+
+ GenericDocument genericDocument = GenericDocument.fromDocumentClass(potentialAction);
+ assertThat(genericDocument.getSchemaType()).isEqualTo("builtin:PotentialAction");
+ assertThat(genericDocument.getPropertyString("name"))
+ .isEqualTo("actions.intent.CREATE_CALL");
+ assertThat(genericDocument.getPropertyString("description"))
+ .isEqualTo("Call John");
+ assertThat(genericDocument.getPropertyString("uri"))
+ .isEqualTo("tel:555-123-4567");
+ }
+}
diff --git a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/ThingTest.java b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/ThingTest.java
index 9093ced..92198a9 100644
--- a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/ThingTest.java
+++ b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/ThingTest.java
@@ -23,9 +23,9 @@
import org.junit.Test;
import java.util.Arrays;
+import java.util.List;
public class ThingTest {
-
@Test
public void testBuilder() {
long now = System.currentTimeMillis();
@@ -39,6 +39,16 @@
.setDescription("this is my first schema.org object")
.setImage("content://images/thing1")
.setUrl("content://things/1")
+ .addPotentialAction(new PotentialAction.Builder()
+ .setName("Start Action")
+ .setDescription("Starts the thing")
+ .setUri("package://start")
+ .build())
+ .addPotentialAction(new PotentialAction.Builder()
+ .setName("Stop Action")
+ .setDescription("Stops the thing")
+ .setUri("package://stop")
+ .build())
.build();
assertThat(thing.getNamespace()).isEqualTo("namespace");
@@ -53,6 +63,17 @@
assertThat(thing.getDescription()).isEqualTo("this is my first schema.org object");
assertThat(thing.getImage()).isEqualTo("content://images/thing1");
assertThat(thing.getUrl()).isEqualTo("content://things/1");
+ assertThat(thing.getPotentialActions()).hasSize(2);
+
+ PotentialAction startAction = thing.getPotentialActions().get(0);
+ assertThat(startAction.getName()).isEqualTo("Start Action");
+ assertThat(startAction.getDescription()).isEqualTo("Starts the thing");
+ assertThat(startAction.getUri()).isEqualTo("package://start");
+
+ PotentialAction stopAction = thing.getPotentialActions().get(1);
+ assertThat(stopAction.getName()).isEqualTo("Stop Action");
+ assertThat(stopAction.getDescription()).isEqualTo("Stops the thing");
+ assertThat(stopAction.getUri()).isEqualTo("package://stop");
}
@Test
@@ -68,6 +89,11 @@
.setDescription("this is my first schema.org object")
.setImage("content://images/thing1")
.setUrl("content://things/1")
+ .addPotentialAction(new PotentialAction.Builder()
+ .setName("Stop Action")
+ .setDescription("Stops the thing")
+ .setUri("package://stop")
+ .build())
.build();
Thing thing2 = new Thing.Builder(thing1).build();
@@ -83,6 +109,12 @@
assertThat(thing2.getDescription()).isEqualTo("this is my first schema.org object");
assertThat(thing2.getImage()).isEqualTo("content://images/thing1");
assertThat(thing2.getUrl()).isEqualTo("content://things/1");
+ assertThat(thing2.getPotentialActions()).isNotNull();
+ assertThat(thing2.getPotentialActions()).hasSize(1);
+ assertThat(thing2.getPotentialActions().get(0).getName()).isEqualTo("Stop Action");
+ assertThat(thing2.getPotentialActions().get(0).getDescription())
+ .isEqualTo("Stops the thing");
+ assertThat(thing2.getPotentialActions().get(0).getUri()).isEqualTo("package://stop");
}
@Test
@@ -98,11 +130,25 @@
.setDescription("this is my first schema.org object")
.setImage("content://images/thing1")
.setUrl("content://things/1")
+ .addPotentialAction(new PotentialAction.Builder()
+ .setDescription("View this thing")
+ .setUri("package://view")
+ .build())
+ .addPotentialAction(new PotentialAction.Builder()
+ .setDescription("Edit this thing")
+ .setUri("package://edit")
+ .build())
.build();
Thing thing2 = new Thing.Builder(thing1)
.clearAlternateNames()
.setImage("content://images/thing2")
.setUrl("content://things/2")
+ .clearPotentialActions()
+ .addPotentialAction(new PotentialAction.Builder()
+ .setName("DeleteAction")
+ .setDescription("Delete this thing")
+ .setUri("package://delete")
+ .build())
.build();
assertThat(thing2.getNamespace()).isEqualTo("namespace");
@@ -115,11 +161,99 @@
assertThat(thing2.getDescription()).isEqualTo("this is my first schema.org object");
assertThat(thing2.getImage()).isEqualTo("content://images/thing2");
assertThat(thing2.getUrl()).isEqualTo("content://things/2");
+
+ List<PotentialAction> potentialActions = thing2.getPotentialActions();
+ assertThat(potentialActions).hasSize(1);
+ assertThat(potentialActions.get(0).getName()).isEqualTo("DeleteAction");
+ assertThat(potentialActions.get(0).getDescription()).isEqualTo("Delete this thing");
+ assertThat(potentialActions.get(0).getUri()).isEqualTo("package://delete");
}
@Test
+ public void testBuilderCopy_builderReuse() {
+ long now = System.currentTimeMillis();
+ Thing.Builder builder = new Thing.Builder("namespace", "thing1")
+ .setDocumentScore(1)
+ .setCreationTimestampMillis(now)
+ .setDocumentTtlMillis(30000)
+ .setName("my first thing")
+ .addAlternateName("my first object")
+ .addAlternateName("माझी पहिली गोष्ट")
+ .setDescription("this is my first schema.org object")
+ .setImage("content://images/thing1")
+ .setUrl("content://things/1")
+ .addPotentialAction(new PotentialAction.Builder()
+ .setDescription("View this thing")
+ .setUri("package://view")
+ .build())
+ .addPotentialAction(new PotentialAction.Builder()
+ .setDescription("Edit this thing")
+ .setUri("package://edit")
+ .build());
+
+ Thing thing1 = builder.build();
+
+ builder.clearAlternateNames()
+ .setImage("content://images/thing2")
+ .setUrl("content://things/2")
+ .clearPotentialActions()
+ .addPotentialAction(new PotentialAction.Builder()
+ .setName("DeleteAction")
+ .setDescription("Delete this thing")
+ .setUri("package://delete")
+ .build());
+
+ Thing thing2 = builder.build();
+
+ // Check that thing1 wasn't altered
+ assertThat(thing1.getNamespace()).isEqualTo("namespace");
+ assertThat(thing1.getId()).isEqualTo("thing1");
+ assertThat(thing1.getDocumentScore()).isEqualTo(1);
+ assertThat(thing1.getCreationTimestampMillis()).isEqualTo(now);
+ assertThat(thing1.getDocumentTtlMillis()).isEqualTo(30000);
+ assertThat(thing1.getName()).isEqualTo("my first thing");
+ assertThat(thing1.getAlternateNames())
+ .containsExactly("my first object", "माझी पहिली गोष्ट");
+ assertThat(thing1.getDescription()).isEqualTo("this is my first schema.org object");
+ assertThat(thing1.getImage()).isEqualTo("content://images/thing1");
+ assertThat(thing1.getUrl()).isEqualTo("content://things/1");
+
+ List<PotentialAction> actions1 = thing1.getPotentialActions();
+ assertThat(actions1).hasSize(2);
+ assertThat(actions1.get(0).getDescription()).isEqualTo("View this thing");
+ assertThat(actions1.get(0).getUri()).isEqualTo("package://view");
+ assertThat(actions1.get(1).getDescription()).isEqualTo("Edit this thing");
+ assertThat(actions1.get(1).getUri()).isEqualTo("package://edit");
+
+ // Check that thing2 has the new values
+ assertThat(thing2.getNamespace()).isEqualTo("namespace");
+ assertThat(thing2.getId()).isEqualTo("thing1");
+ assertThat(thing2.getDocumentScore()).isEqualTo(1);
+ assertThat(thing2.getCreationTimestampMillis()).isEqualTo(now);
+ assertThat(thing2.getDocumentTtlMillis()).isEqualTo(30000);
+ assertThat(thing2.getName()).isEqualTo("my first thing");
+ assertThat(thing2.getAlternateNames()).isEmpty();
+ assertThat(thing2.getDescription()).isEqualTo("this is my first schema.org object");
+ assertThat(thing2.getImage()).isEqualTo("content://images/thing2");
+ assertThat(thing2.getUrl()).isEqualTo("content://things/2");
+
+ List<PotentialAction> actions2 = thing2.getPotentialActions();
+ assertThat(actions2).hasSize(1);
+ assertThat(actions2.get(0).getName()).isEqualTo("DeleteAction");
+ assertThat(actions2.get(0).getDescription()).isEqualTo("Delete this thing");
+ assertThat(actions2.get(0).getUri()).isEqualTo("package://delete");
+ }
+
+
+ @Test
public void testToGenericDocument() throws Exception {
long now = System.currentTimeMillis();
+ PotentialAction potentialAction = new PotentialAction.Builder()
+ .setDescription("Make a phone call")
+ .setName("actions.intent.CALL")
+ .setUri("package://call")
+ .build();
+
Thing thing = new Thing.Builder("namespace", "thing1")
.setDocumentScore(1)
.setCreationTimestampMillis(now)
@@ -130,6 +264,7 @@
.setDescription("this is my first schema.org object")
.setImage("content://images/thing1")
.setUrl("content://things/1")
+ .addPotentialAction(potentialAction)
.build();
GenericDocument document = GenericDocument.fromDocumentClass(thing);
@@ -147,5 +282,12 @@
.isEqualTo("this is my first schema.org object");
assertThat(document.getPropertyString("image")).isEqualTo("content://images/thing1");
assertThat(document.getPropertyString("url")).isEqualTo("content://things/1");
+
+ assertThat(document.getPropertyString("potentialActions[0].name"))
+ .isEqualTo("actions.intent.CALL");
+ assertThat(document.getPropertyString("potentialActions[0].description"))
+ .isEqualTo("Make a phone call");
+ assertThat(document.getPropertyString("potentialActions[0].uri"))
+ .isEqualTo("package://call");
}
}
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Alarm.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Alarm.java
index da4a47a..4c4a2bb 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Alarm.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Alarm.java
@@ -65,12 +65,13 @@
long creationTimestampMillis, long documentTtlMillis, @Nullable String name,
@Nullable List<String> alternateNames, @Nullable String description,
@Nullable String image, @Nullable String url,
+ @NonNull List<PotentialAction> potentialActions,
boolean enabled, @Nullable int[] daysOfWeek, int hour, int minute,
@Nullable String blackoutPeriodStartDate, @Nullable String blackoutPeriodEndDate,
@Nullable String ringtone, boolean shouldVibrate,
@Nullable AlarmInstance previousInstance, @Nullable AlarmInstance nextInstance) {
super(namespace, id, documentScore, creationTimestampMillis, documentTtlMillis, name,
- alternateNames, description, image, url);
+ alternateNames, description, image, url, potentialActions);
mEnabled = enabled;
mDaysOfWeek = daysOfWeek;
mHour = hour;
@@ -396,6 +397,7 @@
public Alarm build() {
return new Alarm(mNamespace, mId, mDocumentScore, mCreationTimestampMillis,
mDocumentTtlMillis, mName, mAlternateNames, mDescription, mImage, mUrl,
+ mPotentialActions,
mEnabled, mDaysOfWeek, mHour, mMinute, mBlackoutPeriodStartDate,
mBlackoutPeriodEndDate, mRingtone, mShouldVibrate, mPreviousInstance,
mNextInstance);
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/AlarmInstance.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/AlarmInstance.java
index 59a142a..edab549 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/AlarmInstance.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/AlarmInstance.java
@@ -74,10 +74,11 @@
AlarmInstance(@NonNull String namespace, @NonNull String id, int documentScore,
long creationTimestampMillis, long documentTtlMillis, @Nullable String name,
@Nullable List<String> alternateNames, @Nullable String description,
- @Nullable String image, @Nullable String url, @NonNull String scheduledTime,
+ @Nullable String image, @Nullable String url,
+ @NonNull List<PotentialAction> potentialActions, @NonNull String scheduledTime,
int status, long snoozeDurationMillis) {
super(namespace, id, documentScore, creationTimestampMillis, documentTtlMillis, name,
- alternateNames, description, image, url);
+ alternateNames, description, image, url, potentialActions);
mScheduledTime = Preconditions.checkNotNull(scheduledTime);
mStatus = status;
mSnoozeDurationMillis = snoozeDurationMillis;
@@ -197,6 +198,7 @@
public AlarmInstance build() {
return new AlarmInstance(mNamespace, mId, mDocumentScore, mCreationTimestampMillis,
mDocumentTtlMillis, mName, mAlternateNames, mDescription, mImage, mUrl,
+ mPotentialActions,
mScheduledTime, mStatus, mSnoozeDurationMillis);
}
}
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/ContactPoint.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/ContactPoint.java
index 84333a6..739e5a6 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/ContactPoint.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/ContactPoint.java
@@ -57,12 +57,13 @@
@Nullable String description,
@Nullable String image,
@Nullable String url,
+ @NonNull List<PotentialAction> potentialActions,
@NonNull String label,
@NonNull List<String> addresses,
@NonNull List<String> emails,
@NonNull List<String> telephones) {
super(namespace, id, documentScore, creationTimestampMillis, documentTtlMillis, name,
- alternateNames, description, image, url);
+ alternateNames, description, image, url, potentialActions);
mLabel = label;
mAddresses = Collections.unmodifiableList(addresses);
mEmails = Collections.unmodifiableList(emails);
@@ -181,6 +182,7 @@
/*description=*/ mDescription,
/*image=*/ mImage,
/*url=*/ mUrl,
+ /*potentialActions=*/ mPotentialActions,
/*label=*/ mLabel,
/*addresses=*/ new ArrayList<>(mAddresses),
/*emails=*/ new ArrayList<>(mEmails),
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/ImageObject.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/ImageObject.java
index 2586d9c..b756437 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/ImageObject.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/ImageObject.java
@@ -51,10 +51,12 @@
ImageObject(@NonNull String namespace, @NonNull String id, int documentScore,
long creationTimestampMillis, long documentTtlMillis, @Nullable String name,
@Nullable List<String> alternateNames, @Nullable String description,
- @Nullable String image, @Nullable String url, @NonNull List<Keyword> keywords,
+ @Nullable String image, @Nullable String url,
+ @NonNull List<PotentialAction> potentialActions,
+ @NonNull List<Keyword> keywords,
@Nullable String sha256, @Nullable String thumbnailSha256) {
super(namespace, id, documentScore, creationTimestampMillis, documentTtlMillis, name,
- alternateNames, description, image, url);
+ alternateNames, description, image, url, potentialActions);
mKeywords = checkNotNull(keywords);
mSha256 = sha256;
mThumbnailSha256 = thumbnailSha256;
@@ -157,7 +159,7 @@
public ImageObject build() {
return new ImageObject(mNamespace, mId, mDocumentScore, mCreationTimestampMillis,
mDocumentTtlMillis, mName, mAlternateNames, mDescription, mImage, mUrl,
- new ArrayList<>(mKeywords), mSha256, mThumbnailSha256);
+ mPotentialActions, new ArrayList<>(mKeywords), mSha256, mThumbnailSha256);
}
/**
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Person.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Person.java
index bbd50e29..062f799 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Person.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Person.java
@@ -158,6 +158,7 @@
@Nullable String description,
@Nullable String image,
@Nullable String url,
+ @NonNull List<PotentialAction> potentialActions,
@Nullable String givenName,
@Nullable String middleName,
@Nullable String familyName,
@@ -172,7 +173,7 @@
@NonNull List<String> relations,
@NonNull List<ContactPoint> contactPoints) {
super(namespace, id, documentScore, creationTimestampMillis, documentTtlMillis, name,
- alternateNames, description, image, url);
+ alternateNames, description, image, url, potentialActions);
mGivenName = givenName;
mMiddleName = middleName;
mFamilyName = familyName;
@@ -524,6 +525,7 @@
/*description=*/ mDescription,
/*image=*/ mImage,
/*url=*/ mUrl,
+ /*potentialActions=*/ mPotentialActions,
/*givenName=*/ mGivenName,
/*middleName=*/ mMiddleName,
/*familyName=*/ mFamilyName,
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/PotentialAction.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/PotentialAction.java
new file mode 100644
index 0000000..7d4e7e4
--- /dev/null
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/PotentialAction.java
@@ -0,0 +1,169 @@
+/*
+ * 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.builtintypes;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appsearch.annotation.Document;
+import androidx.core.util.Preconditions;
+
+/**
+ * An AppSearch document representing an action. This action schema type is used for the nested
+ * potentialActions field in entity schema types such as builtin:Thing or builtin:Timer.
+ *
+ * <ul>
+ * <li><b>name</b> - This is a unique identifier for the action, such as
+ * "actions.intent.CREATE_CALL". See <a
+ * href=developer.android.com/reference/app-actions/built-in-intents/communications/create-call>
+ * developer.android.com/reference/app-actions/built-in-intents/communications/create-call</a>
+ * for an sample Action type.
+ * </li>
+ * <li><b>description</b> - A brief description of what the action does, such as "Create call" or
+ * "Create Message".</li>
+ * <li><b>uri</b> - A deeplink URI linking to an action. Invoking the action can be done by
+ * creating an {@link android.content.Intent} object by calling
+ * {@link android.content.Intent#parseUri} with the deeplink URI. Creating a deeplink URI, and
+ * adding intent extras, can be done by building an intent and calling
+ * {@link android.content.Intent#toUri}.
+ * </li>
+ * </ul>
+ */
+// TODO(b/274671459): Add additional information, if needed, to dispatch actions.
+@Document(name = "builtin:PotentialAction")
+public class PotentialAction {
+ @Document.Namespace
+ final String mNamespace;
+
+ @Document.Id
+ final String mId;
+
+ @Document.StringProperty
+ private final String mName;
+
+ @Document.StringProperty
+ private final String mDescription;
+
+ @Document.StringProperty
+ private final String mUri;
+
+ PotentialAction(@NonNull String namespace, @NonNull String id, @Nullable String name,
+ @Nullable String description, @Nullable String uri) {
+ mNamespace = Preconditions.checkNotNull(namespace);
+ mId = Preconditions.checkNotNull(id);
+ mName = name;
+ mDescription = description;
+ mUri = uri;
+ }
+
+ /** Returns a string describing the action. */
+ @Nullable
+ public String getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * Returns the BII action ID, which comes from Action IDs of Built-in intents listed at <a
+ * href=developer.android.com/reference/app-actions/built-in-intents/bii-index>
+ * developer.android.com/reference/app-actions/built-in-intents/bii-index</a>. For example,
+ * the "Start Exercise" BII has an action id of "actions.intent.START_EXERCISE".
+ */
+ @Nullable
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the deeplink URI.
+ *
+ * <p> A deeplink URI is a URI that lets a user access a specific content or feature within an
+ * app directly. Users can create one by adding parameters to the app's base URI. To use a
+ * deeplink URI in an Android application, users can create an {@link android.content.Intent}
+ * object by calling {@link android.content.Intent#parseUri} with the deeplink URI. Creating a
+ * deeplink URI, and adding intent extras, can be done by building an intent and calling
+ * {@link android.content.Intent#toUri}.
+ */
+ @Nullable
+ public String getUri() {
+ return mUri;
+ }
+
+ /** Builder for {@link PotentialAction}. */
+ public static final class Builder {
+ @Nullable private String mName;
+ @Nullable private String mDescription;
+ @Nullable private String mUri;
+
+ /**
+ * Constructor for {@link PotentialAction.Builder}.
+ *
+ * <p> As PotentialAction is used as a DocumentProperty of Thing, it does not need an id or
+ * namespace.
+ */
+ public Builder() { }
+
+ /**
+ * Constructor with all the existing values.
+ *
+ * <p> As PotentialAction is used as a DocumentProperty of Thing, it does not need an id or
+ * namespace.
+ */
+ public Builder(@NonNull PotentialAction potentialAction) {
+ mName = potentialAction.getName();
+ mDescription = potentialAction.getDescription();
+ mUri = potentialAction.getUri();
+ }
+
+ /** Sets the name of the action. */
+ @NonNull
+ public Builder setName(@Nullable String name) {
+ mName = name;
+ return this;
+ }
+
+ /** Sets the description of the action, such as "Call". */
+ @NonNull
+ public Builder setDescription(@Nullable String description) {
+ mDescription = description;
+ return this;
+ }
+
+ /**
+ * Sets the deeplink URI of the Action.
+ *
+ * <p> A deeplink URI is a URI that lets a user access a specific content or feature within
+ * an app directly. Users can create one by adding parameters to the app's base URI. To use
+ * a deeplink URI in an Android application, users can create an
+ * {@link android.content.Intent} object by calling
+ * {@link android.content.Intent#parseUri} with the deeplink URI. Creating a deeplink URI,
+ * and adding intent extras, can be done by building an intent and calling
+ * {@link android.content.Intent#toUri}.
+ */
+ @NonNull
+ public Builder setUri(@Nullable String uri) {
+ mUri = uri;
+ return this;
+ }
+
+ /** Builds the {@link PotentialAction}. */
+ @NonNull
+ public PotentialAction build() {
+ // As PotentialAction is used as a DocumentProperty of Thing, it does not need an id or
+ // namespace.
+ return new PotentialAction(/*namespace=*/"", /*id=*/"", mName, mDescription, mUri);
+ }
+ }
+}
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Stopwatch.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Stopwatch.java
index a3fc290..6c049b8 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Stopwatch.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Stopwatch.java
@@ -77,11 +77,12 @@
Stopwatch(@NonNull String namespace, @NonNull String id, int documentScore,
long creationTimestampMillis, long documentTtlMillis, @Nullable String name,
@Nullable List<String> alternateNames, @Nullable String description,
- @Nullable String image, @Nullable String url, long baseTimeMillis,
- long baseTimeMillisInElapsedRealtime, int bootCount, int status,
+ @Nullable String image, @Nullable String url,
+ @NonNull List<PotentialAction> potentialActions,
+ long baseTimeMillis, long baseTimeMillisInElapsedRealtime, int bootCount, int status,
long accumulatedDurationMillis, @NonNull List<StopwatchLap> laps) {
super(namespace, id, documentScore, creationTimestampMillis, documentTtlMillis, name,
- alternateNames, description, image, url);
+ alternateNames, description, image, url, potentialActions);
mBaseTimeMillis = baseTimeMillis;
mBaseTimeMillisInElapsedRealtime = baseTimeMillisInElapsedRealtime;
mBootCount = bootCount;
@@ -318,6 +319,7 @@
public Stopwatch build() {
return new Stopwatch(mNamespace, mId, mDocumentScore, mCreationTimestampMillis,
mDocumentTtlMillis, mName, mAlternateNames, mDescription, mImage, mUrl,
+ mPotentialActions,
mBaseTimeMillis, mBaseTimeMillisInElapsedRealtime, mBootCount, mStatus,
mAccumulatedDurationMillis, mLaps);
}
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/StopwatchLap.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/StopwatchLap.java
index 1923629..f5b979a 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/StopwatchLap.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/StopwatchLap.java
@@ -45,10 +45,11 @@
StopwatchLap(@NonNull String namespace, @NonNull String id, int documentScore,
long creationTimestampMillis, long documentTtlMillis, @Nullable String name,
@Nullable List<String> alternateNames, @Nullable String description,
- @Nullable String image, @Nullable String url, int lapNumber,
- long lapDurationMillis, long accumulatedLapDurationMillis) {
+ @Nullable String image, @Nullable String url,
+ @NonNull List<PotentialAction> potentialActions,
+ int lapNumber, long lapDurationMillis, long accumulatedLapDurationMillis) {
super(namespace, id, documentScore, creationTimestampMillis, documentTtlMillis, name,
- alternateNames, description, image, url);
+ alternateNames, description, image, url, potentialActions);
mLapNumber = lapNumber;
mLapDurationMillis = lapDurationMillis;
mAccumulatedLapDurationMillis = accumulatedLapDurationMillis;
@@ -146,6 +147,7 @@
public StopwatchLap build() {
return new StopwatchLap(mNamespace, mId, mDocumentScore, mCreationTimestampMillis,
mDocumentTtlMillis, mName, mAlternateNames, mDescription, mImage, mUrl,
+ mPotentialActions,
mLapNumber, mLapDurationMillis, mAccumulatedLapDurationMillis);
}
}
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Thing.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Thing.java
index 58629bc..954e949 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Thing.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Thing.java
@@ -65,10 +65,14 @@
@Document.StringProperty
private final String mUrl;
+ @Document.DocumentProperty
+ private final List<PotentialAction> mPotentialActions;
+
Thing(@NonNull String namespace, @NonNull String id, int documentScore,
long creationTimestampMillis, long documentTtlMillis, @Nullable String name,
@Nullable List<String> alternateNames, @Nullable String description,
- @Nullable String image, @Nullable String url) {
+ @Nullable String image, @Nullable String url,
+ @Nullable List<PotentialAction> potentialActions) {
mNamespace = Preconditions.checkNotNull(namespace);
mId = Preconditions.checkNotNull(id);
mDocumentScore = documentScore;
@@ -85,6 +89,13 @@
mDescription = description;
mImage = image;
mUrl = url;
+ // AppSearch may pass null if old schema lacks the potentialActions field during
+ // GenericDocument to Java class conversion.
+ if (potentialActions == null) {
+ mPotentialActions = Collections.emptyList();
+ } else {
+ mPotentialActions = Collections.unmodifiableList(potentialActions);
+ }
}
/** Returns the namespace (or logical grouping) for this item. */
@@ -154,6 +165,12 @@
return mUrl;
}
+ /** Returns the actions that can be taken on this object. */
+ @NonNull
+ public List<PotentialAction> getPotentialActions() {
+ return mPotentialActions;
+ }
+
/** Builder for {@link Thing}. */
public static final class Builder extends BuilderImpl<Builder> {
/** Constructs {@link Thing.Builder} with given {@code namespace} and {@code id} */
@@ -181,6 +198,8 @@
protected String mDescription;
protected String mImage;
protected String mUrl;
+ protected List<PotentialAction> mPotentialActions = new ArrayList<>();
+ private boolean mBuilt = false;
BuilderImpl(@NonNull String namespace, @NonNull String id) {
mNamespace = Preconditions.checkNotNull(namespace);
@@ -201,6 +220,7 @@
mDescription = thing.getDescription();
mImage = thing.getImage();
mUrl = thing.getUrl();
+ mPotentialActions = new ArrayList<>(thing.getPotentialActions());
}
/**
@@ -214,6 +234,7 @@
@NonNull
@SuppressWarnings("unchecked")
public T setDocumentScore(int documentScore) {
+ resetIfBuilt();
mDocumentScore = documentScore;
return (T) this;
}
@@ -233,6 +254,7 @@
@NonNull
@SuppressWarnings("unchecked")
public T setCreationTimestampMillis(long creationTimestampMillis) {
+ resetIfBuilt();
mCreationTimestampMillis = creationTimestampMillis;
return (T) this;
}
@@ -251,6 +273,7 @@
@NonNull
@SuppressWarnings("unchecked")
public T setDocumentTtlMillis(long documentTtlMillis) {
+ resetIfBuilt();
mDocumentTtlMillis = documentTtlMillis;
return (T) this;
}
@@ -258,6 +281,7 @@
/** Sets the name of the item. */
@NonNull
public T setName(@Nullable String name) {
+ resetIfBuilt();
mName = name;
return (T) this;
}
@@ -265,6 +289,7 @@
/** Adds an alias for the item. */
@NonNull
public T addAlternateName(@NonNull String alternateName) {
+ resetIfBuilt();
Preconditions.checkNotNull(alternateName);
mAlternateNames.add(alternateName);
return (T) this;
@@ -273,6 +298,7 @@
/** Clears the aliases, if any, for the item. */
@NonNull
public T clearAlternateNames() {
+ resetIfBuilt();
mAlternateNames.clear();
return (T) this;
}
@@ -280,6 +306,7 @@
/** Sets the description for the item. */
@NonNull
public T setDescription(@Nullable String description) {
+ resetIfBuilt();
mDescription = description;
return (T) this;
}
@@ -287,6 +314,7 @@
/** Sets the URL for an image of the item. */
@NonNull
public T setImage(@Nullable String image) {
+ resetIfBuilt();
mImage = image;
return (T) this;
}
@@ -307,15 +335,46 @@
*/
@NonNull
public T setUrl(@Nullable String url) {
+ resetIfBuilt();
mUrl = url;
return (T) this;
}
+ /**
+ * Add a new action to the list of potential actions for this document.
+ */
+ @NonNull
+ public T addPotentialAction(@NonNull PotentialAction newPotentialAction) {
+ resetIfBuilt();
+ Preconditions.checkNotNull(newPotentialAction);
+ mPotentialActions.add(newPotentialAction);
+ return (T) this;
+ }
+
+ /**
+ * Clear all the potential actions for this document.
+ */
+ @NonNull
+ public T clearPotentialActions() {
+ resetIfBuilt();
+ mPotentialActions.clear();
+ return (T) this;
+ }
+
+ private void resetIfBuilt() {
+ if (mBuilt) {
+ mAlternateNames = new ArrayList<>(mAlternateNames);
+ mPotentialActions = new ArrayList<>(mPotentialActions);
+ mBuilt = false;
+ }
+ }
/** Builds a {@link Thing} object. */
@NonNull
public Thing build() {
+ mBuilt = true;
return new Thing(mNamespace, mId, mDocumentScore, mCreationTimestampMillis,
- mDocumentTtlMillis, mName, mAlternateNames, mDescription, mImage, mUrl);
+ mDocumentTtlMillis, mName, mAlternateNames, mDescription, mImage, mUrl,
+ mPotentialActions);
}
}
}
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java
index 11d8961..ef842e4 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java
@@ -91,12 +91,13 @@
long creationTimestampMillis, long documentTtlMillis, @Nullable String name,
@Nullable List<String> alternateNames, @Nullable String description,
@Nullable String image, @Nullable String url,
+ @Nullable List<PotentialAction> potentialActions,
long durationMillis, long originalDurationMillis, long startTimeMillis,
long baseTimeMillis, long baseTimeMillisInElapsedRealtime, int bootCount,
long remainingDurationMillis, @Nullable String ringtone, int status,
boolean shouldVibrate) {
super(namespace, id, documentScore, creationTimestampMillis, documentTtlMillis, name,
- alternateNames, description, image, url);
+ alternateNames, description, image, url, potentialActions);
mDurationMillis = durationMillis;
mOriginalDurationMillis = originalDurationMillis;
mStartTimeMillis = startTimeMillis;
@@ -477,6 +478,7 @@
public Timer build() {
return new Timer(mNamespace, mId, mDocumentScore, mCreationTimestampMillis,
mDocumentTtlMillis, mName, mAlternateNames, mDescription, mImage, mUrl,
+ mPotentialActions,
mDurationMillis, mOriginalDurationMillis, mStartTimeMillis, mBaseTimeMillis,
mBaseTimeMillisInElapsedRealtime, mBootCount, mRemainingDurationMillis,
mRingtone, mStatus, mShouldVibrate);
diff --git a/appsearch/appsearch-debug-view/build.gradle b/appsearch/appsearch-debug-view/build.gradle
index 1b0c3f2..91cc056 100644
--- a/appsearch/appsearch-debug-view/build.gradle
+++ b/appsearch/appsearch-debug-view/build.gradle
@@ -44,7 +44,7 @@
}
androidx {
- name = "AndroidX AppSearch Debug View"
+ name = "AppSearch Debug View"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2021"
description = "A support library for AndroidX AppSearch that contains activities and views " +
diff --git a/appsearch/appsearch-debug-view/samples/build.gradle b/appsearch/appsearch-debug-view/samples/build.gradle
index 6724661..532b284 100644
--- a/appsearch/appsearch-debug-view/samples/build.gradle
+++ b/appsearch/appsearch-debug-view/samples/build.gradle
@@ -46,7 +46,7 @@
}
androidx {
- name = "AndroidX AppSearch Debug View Sample App"
+ name = "AppSearch Debug View Sample App"
type = LibraryType.SAMPLES
inceptionYear = "2021"
description = "Contains a sample app for integrating the Androidx AppSearch Debug View"
diff --git a/appsearch/appsearch-ktx/build.gradle b/appsearch/appsearch-ktx/build.gradle
index fd812db..7b75083 100644
--- a/appsearch/appsearch-ktx/build.gradle
+++ b/appsearch/appsearch-ktx/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = 'AndroidX AppSearch - Kotlin Extensions'
+ name = "AppSearch - Kotlin Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = '2021'
description = 'AndroidX AppSearch - Kotlin Extensions'
diff --git a/appsearch/appsearch-local-storage/proguard-rules.pro b/appsearch/appsearch-local-storage/proguard-rules.pro
index 82c4b719..335e9e8 100644
--- a/appsearch/appsearch-local-storage/proguard-rules.pro
+++ b/appsearch/appsearch-local-storage/proguard-rules.pro
@@ -19,7 +19,7 @@
<fields>;
}
-keep class com.google.android.icing.BreakIteratorBatcher { *; }
--keepclassmembers public class com.google.android.icing.IcingSearchEngine {
+-keepclassmembers public class com.google.android.icing.IcingSearchEngineImpl {
private long nativePointer;
}
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 63bba79..daf7d3a 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
@@ -113,6 +113,7 @@
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null,
ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -493,8 +494,8 @@
InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
mAppSearchImpl.close();
mAppSearchImpl = AppSearchImpl.create(
- mAppSearchDir, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ mAppSearchDir, new UnlimitedLimitConfig(), new DefaultIcingOptionsConfig(),
+ initStatsBuilder, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
// Check recovery state
InitializeStats initStats = initStatsBuilder.build();
@@ -2491,6 +2492,7 @@
AppSearchImpl appSearchImpl2 = AppSearchImpl.create(
mAppSearchDir,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null,
ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -2560,6 +2562,7 @@
AppSearchImpl appSearchImpl2 = AppSearchImpl.create(
mAppSearchDir,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null,
ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -2636,6 +2639,7 @@
AppSearchImpl appSearchImpl2 = AppSearchImpl.create(
mAppSearchDir,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null,
ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -2763,6 +2767,7 @@
return Integer.MAX_VALUE;
}
},
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -2843,6 +2848,7 @@
return Integer.MAX_VALUE;
}
},
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -2901,6 +2907,7 @@
return Integer.MAX_VALUE;
}
},
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -2939,6 +2946,7 @@
return Integer.MAX_VALUE;
}
},
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -3053,6 +3061,7 @@
return Integer.MAX_VALUE;
}
},
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -3150,6 +3159,7 @@
return Integer.MAX_VALUE;
}
},
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -3208,6 +3218,7 @@
return Integer.MAX_VALUE;
}
},
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -3328,7 +3339,10 @@
@Test
public void testRemoveByQuery_withJoinSpec_throwsException() {
Exception e = assertThrows(IllegalArgumentException.class,
- () -> mAppSearchImpl.removeByQuery("", "", "",
+ () -> mAppSearchImpl.removeByQuery(
+ /*packageName=*/"",
+ /*databaseName=*/"",
+ /*queryExpression=*/"",
new SearchSpec.Builder()
.setJoinSpec(new JoinSpec.Builder("childProp").build())
.build(),
@@ -3359,6 +3373,7 @@
return Integer.MAX_VALUE;
}
},
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -3443,6 +3458,7 @@
return Integer.MAX_VALUE;
}
},
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -3501,6 +3517,7 @@
return Integer.MAX_VALUE;
}
},
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -3548,6 +3565,7 @@
return 2;
}
},
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -3649,6 +3667,7 @@
mAppSearchImpl = AppSearchImpl.create(
tempFolder,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null,
ALWAYS_OPTIMIZE,
mockVisibilityChecker);
@@ -3699,6 +3718,7 @@
mAppSearchImpl = AppSearchImpl.create(
tempFolder,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null,
ALWAYS_OPTIMIZE,
mockVisibilityChecker);
@@ -3747,6 +3767,7 @@
mAppSearchImpl = AppSearchImpl.create(
tempFolder,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null,
ALWAYS_OPTIMIZE,
mockVisibilityChecker);
@@ -3797,6 +3818,7 @@
mAppSearchImpl = AppSearchImpl.create(
tempFolder,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null,
ALWAYS_OPTIMIZE,
mockVisibilityChecker);
@@ -4139,6 +4161,7 @@
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null,
ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -4177,6 +4200,7 @@
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null,
ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -4207,6 +4231,7 @@
mAppSearchImpl = AppSearchImpl.create(
tempFolder,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null,
ALWAYS_OPTIMIZE,
mockVisibilityChecker);
@@ -4302,6 +4327,7 @@
mAppSearchImpl = AppSearchImpl.create(
tempFolder,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null,
ALWAYS_OPTIMIZE,
mockVisibilityChecker);
@@ -4388,6 +4414,7 @@
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/null,
ALWAYS_OPTIMIZE,
rejectChecker);
@@ -4489,6 +4516,7 @@
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/null,
ALWAYS_OPTIMIZE,
visibilityChecker);
@@ -4548,6 +4576,7 @@
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/null,
ALWAYS_OPTIMIZE,
rejectChecker);
@@ -4873,6 +4902,7 @@
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/null,
ALWAYS_OPTIMIZE,
visibilityChecker);
@@ -5028,6 +5058,7 @@
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/null,
ALWAYS_OPTIMIZE,
visibilityChecker);
@@ -5114,6 +5145,7 @@
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/null,
ALWAYS_OPTIMIZE,
visibilityChecker);
@@ -5204,6 +5236,7 @@
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/null,
ALWAYS_OPTIMIZE,
visibilityChecker);
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 bca29e4..c9828f5 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
@@ -22,6 +22,7 @@
import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.JoinSpec;
import androidx.appsearch.app.SearchResultPage;
import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.exceptions.AppSearchException;
@@ -54,6 +55,7 @@
import org.junit.rules.TemporaryFolder;
import java.io.File;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -74,6 +76,7 @@
mAppSearchImpl = AppSearchImpl.create(
mTemporaryFolder.newFolder(),
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null,
ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -348,6 +351,7 @@
AppSearchImpl appSearchImpl = AppSearchImpl.create(
mTemporaryFolder.newFolder(),
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
initStatsBuilder,
ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -378,6 +382,7 @@
AppSearchImpl appSearchImpl = AppSearchImpl.create(
folder,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null,
ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
@@ -414,8 +419,8 @@
// Create another appsearchImpl on the same folder
InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
appSearchImpl = AppSearchImpl.create(
- folder, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ folder, new UnlimitedLimitConfig(), new DefaultIcingOptionsConfig(),
+ initStatsBuilder, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
InitializeStats iStats = initStatsBuilder.build();
assertThat(iStats).isNotNull();
@@ -441,8 +446,8 @@
final File folder = mTemporaryFolder.newFolder();
AppSearchImpl appSearchImpl = AppSearchImpl.create(
- folder, new UnlimitedLimitConfig(), /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ folder, new UnlimitedLimitConfig(), new DefaultIcingOptionsConfig(),
+ /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
List<AppSearchSchema> schemas = ImmutableList.of(
new AppSearchSchema.Builder("Type1").build(),
@@ -480,8 +485,8 @@
// Create another appsearchImpl on the same folder
InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
appSearchImpl = AppSearchImpl.create(
- folder, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
+ folder, new UnlimitedLimitConfig(), new DefaultIcingOptionsConfig(),
+ initStatsBuilder, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
InitializeStats iStats = initStatsBuilder.build();
// Some of other fields are already covered by AppSearchImplTest#testReset()
@@ -708,6 +713,160 @@
}
@Test
+ public void testLoggingStats_search_join() throws Exception {
+ AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("entityId")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig
+ .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .build()
+ ).build();
+
+ AppSearchSchema entitySchema = new AppSearchSchema.Builder("entity")
+ .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();
+ List<AppSearchSchema> schemas = Arrays.asList(actionSchema, entitySchema);
+
+ // Insert schema
+ final String testPackageName = "testPackage";
+ final String testDatabase = "testDatabase";
+ InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+ testPackageName,
+ testDatabase,
+ schemas,
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /* setSchemaStatsBuilder= */ null);
+
+ assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
+ GenericDocument entity1 =
+ new GenericDocument.Builder<>("namespace", "id1", "entity")
+ .setPropertyString("subject", "an entity")
+ .build();
+ GenericDocument entity2 =
+ new GenericDocument.Builder<>("namespace", "id2", "entity")
+ .setPropertyString("subject", "another entity")
+ .build();
+
+ GenericDocument action1 =
+ new GenericDocument.Builder<>("namespace", "action1", "ViewAction")
+ .setPropertyString("entityId",
+ "testPackage$testDatabase/namespace#id1")
+ .build();
+ GenericDocument action2 =
+ new GenericDocument.Builder<>("namespace", "action2", "ViewAction")
+ .setPropertyString("entityId",
+ "testPackage$testDatabase/namespace#id1")
+ .build();
+ GenericDocument action3 =
+ new GenericDocument.Builder<>("namespace", "action3", "ViewAction")
+ .setPropertyString("entityId",
+ "testPackage$testDatabase/namespace#id1")
+ .build();
+ GenericDocument action4 =
+ new GenericDocument.Builder<>("namespace", "action4", "ViewAction")
+ .setPropertyString("entityId",
+ "testPackage$testDatabase/namespace#id2")
+ .build();
+
+ mAppSearchImpl.putDocument(
+ testPackageName,
+ testDatabase,
+ entity1,
+ /*sendChangeNotifications=*/ false,
+ mLogger);
+ mAppSearchImpl.putDocument(
+ testPackageName,
+ testDatabase,
+ entity2,
+ /*sendChangeNotifications=*/ false,
+ mLogger);
+ mAppSearchImpl.putDocument(
+ testPackageName,
+ testDatabase,
+ action1,
+ /*sendChangeNotifications=*/ false,
+ mLogger);
+ mAppSearchImpl.putDocument(
+ testPackageName,
+ testDatabase,
+ action2,
+ /*sendChangeNotifications=*/ false,
+ mLogger);
+ mAppSearchImpl.putDocument(
+ testPackageName,
+ testDatabase,
+ action3,
+ /*sendChangeNotifications=*/ false,
+ mLogger);
+ mAppSearchImpl.putDocument(
+ testPackageName,
+ testDatabase,
+ action4,
+ /*sendChangeNotifications=*/ false,
+ mLogger);
+
+ SearchSpec nestedSearchSpec =
+ new SearchSpec.Builder()
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
+ .setOrder(SearchSpec.ORDER_ASCENDING)
+ .build();
+
+ JoinSpec js = new JoinSpec.Builder("entityId")
+ .setNestedSearch("", nestedSearchSpec)
+ .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+ .build();
+
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+ .setJoinSpec(js)
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .build();
+
+ String queryStr = "entity";
+ SearchResultPage searchResultPage = mAppSearchImpl.query(testPackageName, testDatabase,
+ queryStr, searchSpec, /*logger=*/ mLogger);
+
+ assertThat(searchResultPage.getResults()).hasSize(2);
+ assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(entity1);
+ assertThat(searchResultPage.getResults().get(1).getGenericDocument()).isEqualTo(entity2);
+
+ SearchStats sStats = mLogger.mSearchStats;
+
+ assertThat(sStats).isNotNull();
+
+ assertThat(sStats.getPackageName()).isEqualTo(testPackageName);
+ assertThat(sStats.getDatabase()).isEqualTo(testDatabase);
+ assertThat(sStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
+ assertThat(sStats.getVisibilityScope()).isEqualTo(SearchStats.VISIBILITY_SCOPE_LOCAL);
+ assertThat(sStats.getTermCount()).isEqualTo(1);
+ assertThat(sStats.getQueryLength()).isEqualTo(queryStr.length());
+ assertThat(sStats.getFilteredNamespaceCount()).isEqualTo(1);
+ assertThat(sStats.getFilteredSchemaTypeCount()).isEqualTo(2);
+ assertThat(sStats.getCurrentPageReturnedResultCount()).isEqualTo(2);
+ assertThat(sStats.isFirstPage()).isTrue();
+ assertThat(sStats.getRankingStrategy()).isEqualTo(
+ ScoringSpecProto.RankingStrategy.Code.JOIN_AGGREGATE_SCORE_VALUE);
+ assertThat(sStats.getScoredDocumentCount()).isEqualTo(2);
+ assertThat(sStats.getResultWithSnippetsCount()).isEqualTo(0);
+ // Join-specific stats. If the process goes really fast, the total latency could be 0.
+ // Since the default of total latency is also 0, we just remove the assertion on
+ // JoinLatencyMillis.
+ assertThat(sStats.getJoinType()).isEqualTo(
+ AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);
+ assertThat(sStats.getNumJoinedResultsCurrentPage()).isEqualTo(4);
+ }
+
+ @Test
public void testLoggingStats_remove_success() throws Exception {
// Insert schema
final String testPackageName = "testPackage";
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 dff83a2..6840a98 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
@@ -52,6 +52,7 @@
mAppSearchImpl = AppSearchImpl.create(
mTemporaryFolder.newFolder(),
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
}
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 3f2cb70..95cf9c5 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
@@ -20,6 +20,7 @@
import androidx.appsearch.app.AppSearchSchema;
+import com.google.android.icing.proto.JoinableConfig;
import com.google.android.icing.proto.PropertyConfigProto;
import com.google.android.icing.proto.SchemaTypeConfigProto;
import com.google.android.icing.proto.StringIndexingConfig;
@@ -118,4 +119,41 @@
assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedMusicRecordingProto))
.isEqualTo(musicRecordingSchema);
}
+
+ @Test
+ public void testGetProto_JoinableConfig() {
+ AppSearchSchema albumSchema = new AppSearchSchema.Builder("Album")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("artist")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig
+ .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+ .setDeletionPropagation(true)
+ .build()
+ ).build();
+
+ JoinableConfig joinableConfig = JoinableConfig.newBuilder()
+ .setValueType(JoinableConfig.ValueType.Code.QUALIFIED_ID)
+ .setPropagateDelete(true)
+ .build();
+
+ SchemaTypeConfigProto expectedAlbumProto = SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("Album")
+ .setVersion(0)
+ .addProperties(
+ PropertyConfigProto.newBuilder()
+ .setPropertyName("artist")
+ .setDataType(PropertyConfigProto.DataType.Code.STRING)
+ .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+ .setStringIndexingConfig(StringIndexingConfig.newBuilder()
+ .setTermMatchType(TermMatchType.Code.UNKNOWN)
+ .setTokenizerType(
+ StringIndexingConfig.TokenizerType.Code.NONE))
+ .setJoinableConfig(joinableConfig))
+ .build();
+
+ assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(albumSchema, /*version=*/0))
+ .isEqualTo(expectedAlbumProto);
+ assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedAlbumProto))
+ .isEqualTo(albumSchema);
+ }
}
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 271bf2a..79a9f85 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
@@ -26,6 +26,7 @@
import androidx.appsearch.app.JoinSpec;
import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.DefaultIcingOptionsConfig;
import androidx.appsearch.localstorage.OptimizeStrategy;
import androidx.appsearch.localstorage.UnlimitedLimitConfig;
import androidx.appsearch.localstorage.util.PrefixUtil;
@@ -43,6 +44,8 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import org.junit.After;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -58,6 +61,24 @@
@Rule
public final TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+ private AppSearchImpl mAppSearchImpl;
+
+ @Before
+ public void setUp() throws Exception {
+ mAppSearchImpl = AppSearchImpl.create(
+ mTemporaryFolder.newFolder(),
+ new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
+ /*initStatsBuilder=*/ null,
+ ALWAYS_OPTIMIZE,
+ /*visibilityChecker=*/null);
+ }
+
+ @After
+ public void tearDown() {
+ mAppSearchImpl.close();
+ }
+
@Test
public void testToSearchSpecProto() throws Exception {
SearchSpec searchSpec = new SearchSpec.Builder().build();
@@ -70,19 +91,19 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(prefix1, prefix2),
/*namespaceMap=*/ImmutableMap.of(
- prefix1, ImmutableSet.of(
- prefix1 + "namespace1",
- prefix1 + "namespace2"),
- prefix2, ImmutableSet.of(
- prefix2 + "namespace1",
- prefix2 + "namespace2")),
+ prefix1, ImmutableSet.of(
+ prefix1 + "namespace1",
+ prefix1 + "namespace2"),
+ 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)));
+ prefix1, ImmutableMap.of(
+ prefix1 + "typeA", configProto,
+ prefix1 + "typeB", configProto),
+ prefix2, ImmutableMap.of(
+ prefix2 + "typeA", configProto,
+ prefix2 + "typeB", configProto)));
// Convert SearchSpec to proto.
SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -149,7 +170,7 @@
JoinSpecProto joinSpecProto = searchSpecProto.getJoinSpec();
assertThat(joinSpecProto.hasNestedSpec()).isTrue();
- assertThat(joinSpecProto.getParentPropertyExpression()).isEqualTo(JoinSpec.QUALIFIED_ID);
+ assertThat(joinSpecProto.getParentPropertyExpression()).isEqualTo("this.qualifiedId()");
assertThat(joinSpecProto.getChildPropertyExpression()).isEqualTo("childPropertyExpression");
assertThat(joinSpecProto.getAggregationScoringStrategy())
.isEqualTo(JoinSpecProto.AggregationScoringStrategy.Code.SUM);
@@ -161,6 +182,87 @@
ScoringSpecProto.RankingStrategy.Code.CREATION_TIMESTAMP);
}
+ @Test
+ public void testToSearchSpec_withJoinSpec_childSearchesOtherSchema() throws Exception {
+ String prefix1 = PrefixUtil.createPrefix("package", "database1");
+ String prefix2 = PrefixUtil.createPrefix("package", "database2");
+
+ SearchSpec nestedSearchSpec =
+ new SearchSpec.Builder()
+ .addFilterPackageNames("package")
+ .addFilterSchemas("typeA")
+ .build();
+ SearchSpec.Builder searchSpec =
+ new SearchSpec.Builder()
+ .addFilterPackageNames("package")
+ .addFilterSchemas("typeB");
+
+ // Create a JoinSpec object and set it in the converter
+ JoinSpec joinSpec =
+ new JoinSpec.Builder("childPropertyExpression")
+ .setNestedSearch("nestedQuery", nestedSearchSpec)
+ .setMaxJoinedResultCount(10)
+ .build();
+
+ searchSpec.setJoinSpec(joinSpec);
+
+ SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
+ SearchSpecToProtoConverter converter =
+ new SearchSpecToProtoConverter(
+ /*queryExpression=*/ "query",
+ searchSpec.build(),
+ /*prefixes=*/ ImmutableSet.of(prefix1, prefix2),
+ /*namespaceMap=*/ ImmutableMap.of(
+ prefix1,
+ ImmutableSet.of(
+ prefix1 + "namespace1", prefix1 + "namespace2"),
+ 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)));
+
+ VisibilityStore visibilityStore = new VisibilityStore(mAppSearchImpl);
+ converter.removeInaccessibleSchemaFilter(
+ new CallerAccess(/*callingPackageName=*/"package"),
+ visibilityStore,
+ AppSearchTestUtils.createMockVisibilityChecker(
+ /*visiblePrefixedSchemas=*/ ImmutableSet.of(
+ prefix1 + "typeA", prefix1 + "typeB", prefix2 + "typeA",
+ prefix2 + "typeB")));
+
+ // Convert SearchSpec to proto.
+ SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
+
+ assertThat(searchSpecProto.getQuery()).isEqualTo("query");
+ assertThat(searchSpecProto.getSchemaTypeFiltersList())
+ .containsExactly(
+ "package$database1/typeB",
+ "package$database2/typeB");
+ assertThat(searchSpecProto.getNamespaceFiltersList())
+ .containsExactly(
+ "package$database1/namespace1", "package$database1/namespace2",
+ "package$database2/namespace1", "package$database2/namespace2");
+
+ // Assert that the joinSpecProto is set correctly in the searchSpecProto
+ assertThat(searchSpecProto.hasJoinSpec()).isTrue();
+
+ JoinSpecProto joinSpecProto = searchSpecProto.getJoinSpec();
+ assertThat(joinSpecProto.hasNestedSpec()).isTrue();
+
+ JoinSpecProto.NestedSpecProto nestedSpecProto = joinSpecProto.getNestedSpec();
+ assertThat(nestedSpecProto.getSearchSpec().getSchemaTypeFiltersList())
+ .containsExactly(
+ "package$database1/typeA",
+ "package$database2/typeA");
+ }
@Test
public void testToScoringSpecProto() {
@@ -231,7 +333,8 @@
/*namespaceMap=*/ImmutableMap.of(),
/*schemaMap=*/ImmutableMap.of());
ResultSpecProto resultSpecProto = convert.toResultSpecProto(
- /*namespaceMap=*/ImmutableMap.of());
+ /*namespaceMap=*/ImmutableMap.of(),
+ /*schemaMap=*/ImmutableMap.of());
assertThat(resultSpecProto.getNumPerPage()).isEqualTo(123);
assertThat(resultSpecProto.getSnippetSpec().getNumToSnippet()).isEqualTo(234);
@@ -261,7 +364,8 @@
prefix1 + "namespaceB"),
prefix2, ImmutableSet.of(
prefix2 + "namespaceA",
- prefix2 + "namespaceB")));
+ prefix2 + "namespaceB")),
+ /*schemaMap=*/ImmutableMap.of());
assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(2);
// First grouping should have same package name.
@@ -305,7 +409,9 @@
/*prefixes=*/ImmutableSet.of(prefix1, prefix2),
namespaceMap,
/*schemaMap=*/ImmutableMap.of());
- ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap);
+ ResultSpecProto resultSpecProto = converter.toResultSpecProto(
+ namespaceMap,
+ /*schemaMap=*/ImmutableMap.of());
assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(2);
// First grouping should have same namespace.
@@ -326,10 +432,56 @@
}
@Test
+ public void testToResultSpecProto_groupBySchema() throws Exception {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_SCHEMA, 5)
+ .build();
+
+ String prefix1 = PrefixUtil.createPrefix("package1", "database");
+ String prefix2 = PrefixUtil.createPrefix("package2", "database");
+
+ SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
+ Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ prefix1 + "typeA", configProto,
+ prefix1 + "typeB", configProto),
+ prefix2, ImmutableMap.of(
+ prefix2 + "typeA", configProto,
+ prefix2 + "typeB", configProto));
+
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+ /*queryExpression=*/"query",
+ searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+ /*namespaceMap=*/ImmutableMap.of(),
+ schemaMap);
+ ResultSpecProto resultSpecProto = converter.toResultSpecProto(
+ /*namespaceMap=*/ImmutableMap.of(),
+ schemaMap);
+
+ assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(2);
+ // First grouping should have the same schema type.
+ ResultSpecProto.ResultGrouping grouping1 = resultSpecProto.getResultGroupings(0);
+ assertThat(grouping1.getEntryGroupingsList()).hasSize(2);
+ assertThat(
+ PrefixUtil.removePrefix(grouping1.getEntryGroupings(0).getSchema()))
+ .isEqualTo(
+ PrefixUtil.removePrefix(grouping1.getEntryGroupings(1).getSchema()));
+
+ // Second grouping should have the same schema type.
+ ResultSpecProto.ResultGrouping grouping2 = resultSpecProto.getResultGroupings(1);
+ assertThat(grouping2.getEntryGroupingsList()).hasSize(2);
+ assertThat(
+ PrefixUtil.removePrefix(grouping2.getEntryGroupings(0).getSchema()))
+ .isEqualTo(
+ PrefixUtil.removePrefix(grouping2.getEntryGroupings(1).getSchema()));
+ }
+
+ @Test
public void testToResultSpecProto_groupByNamespaceAndPackage() throws Exception {
SearchSpec searchSpec = new SearchSpec.Builder()
.setResultGrouping(GROUPING_TYPE_PER_PACKAGE
- | SearchSpec.GROUPING_TYPE_PER_NAMESPACE, 5)
+ | SearchSpec.GROUPING_TYPE_PER_NAMESPACE, 5)
.build();
String prefix1 = PrefixUtil.createPrefix("package1", "database");
@@ -347,7 +499,9 @@
searchSpec,
/*prefixes=*/ImmutableSet.of(prefix1, prefix2),
namespaceMap, /*schemaMap=*/ImmutableMap.of());
- ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap);
+ ResultSpecProto resultSpecProto = converter.toResultSpecProto(
+ namespaceMap,
+ /*schemaMap=*/ImmutableMap.of());
// All namespace should be separated.
assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(4);
@@ -358,6 +512,241 @@
}
@Test
+ public void testToResultSpecProto_groupBySchemaAndPackage() throws Exception {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setResultGrouping(GROUPING_TYPE_PER_PACKAGE
+ | SearchSpec.GROUPING_TYPE_PER_SCHEMA, 5)
+ .build();
+
+ String prefix1 = PrefixUtil.createPrefix("package1", "database");
+ String prefix2 = PrefixUtil.createPrefix("package2", "database");
+ SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
+ Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ prefix1 + "typeA", configProto,
+ prefix1 + "typeB", configProto),
+ prefix2, ImmutableMap.of(
+ prefix2 + "typeA", configProto,
+ prefix2 + "typeB", configProto));
+
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+ /*queryExpression=*/"query",
+ searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+ /*namespaceMap=*/ImmutableMap.of(),
+ schemaMap);
+ ResultSpecProto resultSpecProto = converter.toResultSpecProto(
+ /*namespaceMap=*/ImmutableMap.of(),
+ schemaMap);
+
+ // All schema should be separated.
+ assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(4);
+ assertThat(resultSpecProto.getResultGroupings(0).getEntryGroupingsList()).hasSize(1);
+ assertThat(resultSpecProto.getResultGroupings(1).getEntryGroupingsList()).hasSize(1);
+ assertThat(resultSpecProto.getResultGroupings(2).getEntryGroupingsList()).hasSize(1);
+ assertThat(resultSpecProto.getResultGroupings(3).getEntryGroupingsList()).hasSize(1);
+ }
+
+ @Test
+ public void testToResultSpecProto_groupByNamespaceAndSchema() throws Exception {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+ | SearchSpec.GROUPING_TYPE_PER_SCHEMA, 5)
+ .build();
+
+ String prefix1 = PrefixUtil.createPrefix("package1", "database");
+ String prefix2 = PrefixUtil.createPrefix("package2", "database");
+ Map<String, Set<String>> namespaceMap = /*namespaceMap=*/ImmutableMap.of(
+ prefix1, ImmutableSet.of(
+ prefix1 + "namespaceA",
+ prefix1 + "namespaceB"),
+ prefix2, ImmutableSet.of(
+ prefix2 + "namespaceA",
+ prefix2 + "namespaceB"));
+ SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
+ Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ prefix1 + "typeA", configProto,
+ prefix1 + "typeB", configProto),
+ prefix2, ImmutableMap.of(
+ prefix2 + "typeA", configProto,
+ prefix2 + "typeB", configProto));
+
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+ /*queryExpression=*/"query",
+ searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+ namespaceMap,
+ schemaMap);
+ ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap, schemaMap);
+
+ assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(4);
+ ResultSpecProto.ResultGrouping grouping1 = resultSpecProto.getResultGroupings(0);
+ // Each grouping should have a size of 2.
+ assertThat(grouping1.getEntryGroupingsList()).hasSize(2);
+ // Each grouping should have the same namespace and schema type.
+ // Each entry should have the same package and database.
+ assertThat(grouping1.getEntryGroupings(0).getNamespace())
+ .isEqualTo("package1$database/namespaceA");
+ assertThat(grouping1.getEntryGroupings(0).getSchema())
+ .isEqualTo("package1$database/typeA");
+ assertThat(grouping1.getEntryGroupings(1).getNamespace())
+ .isEqualTo("package2$database/namespaceA");
+ assertThat(grouping1.getEntryGroupings(1).getSchema())
+ .isEqualTo("package2$database/typeA");
+
+ ResultSpecProto.ResultGrouping grouping2 = resultSpecProto.getResultGroupings(1);
+ // Each grouping should have a size of 2.
+ assertThat(grouping2.getEntryGroupingsList()).hasSize(2);
+ // Each grouping should have the same namespace and schema type.
+ // Each entry should have the same package and database.
+ assertThat(grouping2.getEntryGroupings(0).getNamespace())
+ .isEqualTo("package1$database/namespaceA");
+ assertThat(grouping2.getEntryGroupings(0).getSchema())
+ .isEqualTo("package1$database/typeB");
+ assertThat(grouping2.getEntryGroupings(1).getNamespace())
+ .isEqualTo("package2$database/namespaceA");
+ assertThat(grouping2.getEntryGroupings(1).getSchema())
+ .isEqualTo("package2$database/typeB");
+
+ ResultSpecProto.ResultGrouping grouping3 = resultSpecProto.getResultGroupings(2);
+ // Each grouping should have a size of 2.
+ assertThat(grouping3.getEntryGroupingsList()).hasSize(2);
+ // Each grouping should have the same namespace and schema type.
+ // Each entry should have the same package and database.
+ assertThat(grouping3.getEntryGroupings(0).getNamespace())
+ .isEqualTo("package1$database/namespaceB");
+ assertThat(grouping3.getEntryGroupings(0).getSchema())
+ .isEqualTo("package1$database/typeA");
+ assertThat(grouping3.getEntryGroupings(1).getNamespace())
+ .isEqualTo("package2$database/namespaceB");
+ assertThat(grouping3.getEntryGroupings(1).getSchema())
+ .isEqualTo("package2$database/typeA");
+
+ ResultSpecProto.ResultGrouping grouping4 = resultSpecProto.getResultGroupings(3);
+ // Each grouping should have a size of 2.
+ assertThat(grouping4.getEntryGroupingsList()).hasSize(2);
+ // Each grouping should have the same namespace and schema type.
+ // Each entry should have the same package and database.
+ assertThat(grouping4.getEntryGroupings(0).getNamespace())
+ .isEqualTo("package1$database/namespaceB");
+ assertThat(grouping4.getEntryGroupings(0).getSchema())
+ .isEqualTo("package1$database/typeB");
+ assertThat(grouping4.getEntryGroupings(1).getNamespace())
+ .isEqualTo("package2$database/namespaceB");
+ assertThat(grouping4.getEntryGroupings(1).getSchema())
+ .isEqualTo("package2$database/typeB");
+ }
+
+ @Test
+ public void testToResultSpecProto_groupByNamespaceAndSchemaAndPackage() throws Exception {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_PACKAGE
+ | SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+ | SearchSpec.GROUPING_TYPE_PER_SCHEMA, 5)
+ .build();
+ String prefix1 = PrefixUtil.createPrefix("package1", "database");
+ String prefix2 = PrefixUtil.createPrefix("package2", "database");
+ Map<String, Set<String>> namespaceMap = /*namespaceMap=*/ImmutableMap.of(
+ prefix1, ImmutableSet.of(
+ prefix1 + "namespaceA",
+ prefix1 + "namespaceB"),
+ prefix2, ImmutableSet.of(
+ prefix2 + "namespaceA",
+ prefix2 + "namespaceB"));
+ SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
+ Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ prefix1 + "typeA", configProto,
+ prefix1 + "typeB", configProto),
+ prefix2, ImmutableMap.of(
+ prefix2 + "typeA", configProto,
+ prefix2 + "typeB", configProto));
+
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+ /*queryExpression=*/"query",
+ searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+ namespaceMap,
+ schemaMap);
+ ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap, schemaMap);
+
+ assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(8);
+ ResultSpecProto.ResultGrouping grouping1 = resultSpecProto.getResultGroupings(0);
+ //assertThat(grouping1.getEntryGroupingsList()).containsExactly();
+ // Each grouping should have the size of 1.
+ assertThat(grouping1.getEntryGroupingsList()).hasSize(1);
+ // Each entry should have the same package.
+ assertThat(grouping1.getEntryGroupings(0).getNamespace())
+ .isEqualTo("package2$database/namespaceA");
+ assertThat(grouping1.getEntryGroupings(0).getSchema())
+ .isEqualTo("package2$database/typeA");
+
+ ResultSpecProto.ResultGrouping grouping2 = resultSpecProto.getResultGroupings(1);
+ // Each grouping should have the size of 1.
+ assertThat(grouping2.getEntryGroupingsList()).hasSize(1);
+ // Each entry should have the same package.
+ assertThat(grouping2.getEntryGroupings(0).getNamespace())
+ .isEqualTo("package2$database/namespaceA");
+ assertThat(grouping2.getEntryGroupings(0).getSchema())
+ .isEqualTo("package2$database/typeB");
+
+ ResultSpecProto.ResultGrouping grouping3 = resultSpecProto.getResultGroupings(2);
+ // Each grouping should have the size of 1.
+ assertThat(grouping3.getEntryGroupingsList()).hasSize(1);
+ // Each entry should have the same package.
+ assertThat(grouping3.getEntryGroupings(0).getNamespace())
+ .isEqualTo("package2$database/namespaceB");
+ assertThat(grouping3.getEntryGroupings(0).getSchema())
+ .isEqualTo("package2$database/typeA");
+
+ ResultSpecProto.ResultGrouping grouping4 = resultSpecProto.getResultGroupings(3);
+ // Each grouping should have the size of 1.
+ assertThat(grouping4.getEntryGroupingsList()).hasSize(1);
+ // Each entry should have the same package.
+ assertThat(grouping4.getEntryGroupings(0).getNamespace())
+ .isEqualTo("package2$database/namespaceB");
+ assertThat(grouping4.getEntryGroupings(0).getSchema())
+ .isEqualTo("package2$database/typeB");
+
+ ResultSpecProto.ResultGrouping grouping5 = resultSpecProto.getResultGroupings(4);
+ // Each grouping should have the size of 1.
+ assertThat(grouping5.getEntryGroupingsList()).hasSize(1);
+ // Each entry should have the same package.
+ assertThat(grouping5.getEntryGroupings(0).getNamespace())
+ .isEqualTo("package1$database/namespaceA");
+ assertThat(grouping5.getEntryGroupings(0).getSchema())
+ .isEqualTo("package1$database/typeA");
+
+ ResultSpecProto.ResultGrouping grouping6 = resultSpecProto.getResultGroupings(5);
+ // Each grouping should have the size of 1.
+ assertThat(grouping6.getEntryGroupingsList()).hasSize(1);
+ // Each entry should have the same package.
+ assertThat(grouping6.getEntryGroupings(0).getNamespace())
+ .isEqualTo("package1$database/namespaceA");
+ assertThat(grouping6.getEntryGroupings(0).getSchema())
+ .isEqualTo("package1$database/typeB");
+
+ ResultSpecProto.ResultGrouping grouping7 = resultSpecProto.getResultGroupings(6);
+ // Each grouping should have the size of 1.
+ assertThat(grouping7.getEntryGroupingsList()).hasSize(1);
+ // Each entry should have the same package.
+ assertThat(grouping7.getEntryGroupings(0).getNamespace())
+ .isEqualTo("package1$database/namespaceB");
+ assertThat(grouping7.getEntryGroupings(0).getSchema())
+ .isEqualTo("package1$database/typeA");
+
+ ResultSpecProto.ResultGrouping grouping8 = resultSpecProto.getResultGroupings(7);
+ // Each grouping should have the size of 1.
+ assertThat(grouping8.getEntryGroupingsList()).hasSize(1);
+ // Each entry should have the same package.
+ assertThat(grouping8.getEntryGroupings(0).getNamespace())
+ .isEqualTo("package1$database/namespaceB");
+ assertThat(grouping8.getEntryGroupings(0).getSchema())
+ .isEqualTo("package1$database/typeB");
+ }
+
+ @Test
public void testGetTargetNamespaceFilters_emptySearchingFilter() {
SearchSpec searchSpec = new SearchSpec.Builder().build();
String prefix1 = PrefixUtil.createPrefix("package", "database1");
@@ -556,20 +945,19 @@
@Test
public void testRemoveInaccessibleSchemaFilter() throws Exception {
- AppSearchImpl appSearchImpl = AppSearchImpl.create(
- mTemporaryFolder.newFolder(),
- new UnlimitedLimitConfig(),
- /*initStatsBuilder=*/null,
- ALWAYS_OPTIMIZE,
- /*visibilityChecker=*/null);
- VisibilityStore visibilityStore = new VisibilityStore(appSearchImpl);
+ VisibilityStore visibilityStore = new VisibilityStore(mAppSearchImpl);
final String prefix = PrefixUtil.createPrefix("package", "database");
SchemaTypeConfigProto schemaTypeConfigProto =
SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
+
+ SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
+ JoinSpec joinSpec = new JoinSpec.Builder("entity")
+ .setNestedSearch("", nestedSearchSpec).build();
+
SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
/*queryExpression=*/"",
- new SearchSpec.Builder().build(),
+ new SearchSpec.Builder().setJoinSpec(joinSpec).build(),
/*prefixes=*/ImmutableSet.of(prefix),
/*namespaceMap=*/ImmutableMap.of(
prefix, ImmutableSet.of("package$database/namespace1")),
@@ -590,12 +978,21 @@
// schema 2 is filtered out since it is not searchable for user.
assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
prefix + "schema1", prefix + "schema3");
+
+ SearchSpecProto nestedSearchProto =
+ searchSpecProto.getJoinSpec().getNestedSpec().getSearchSpec();
+ assertThat(nestedSearchProto.getSchemaTypeFiltersList()).containsExactly(
+ prefix + "schema1", prefix + "schema3");
}
@Test
public void testIsNothingToSearch() {
String prefix = PrefixUtil.createPrefix("package", "database");
+ SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
+ JoinSpec joinSpec = new JoinSpec.Builder("entity")
+ .setNestedSearch("nested", nestedSearchSpec).build();
SearchSpec searchSpec = new SearchSpec.Builder()
+ .setJoinSpec(joinSpec)
.addFilterSchemas("schema").addFilterNamespaces("namespace").build();
// build maps
@@ -638,6 +1035,48 @@
/*visibilityStore=*/null,
/*visibilityChecker=*/null);
assertThat(nonEmptyConverter.hasNothingToSearch()).isTrue();
+ // As the JoinSpec has nothing to search, it should not be part of the SearchSpec
+ assertThat(nonEmptyConverter.toSearchSpecProto().hasJoinSpec()).isFalse();
+ }
+
+ @Test
+ public void testRemoveInaccessibleSchemaFilterWithEmptyNestedFilter() throws Exception {
+ VisibilityStore visibilityStore = new VisibilityStore(mAppSearchImpl);
+
+ final String prefix = PrefixUtil.createPrefix("package", "database");
+ SchemaTypeConfigProto schemaTypeConfigProto =
+ SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
+
+ SearchSpec nestedSearchSpec = new SearchSpec.Builder()
+ .addFilterSchemas(ImmutableSet.of(prefix + "schema1", prefix + "schema2"))
+ .build();
+ JoinSpec joinSpec = new JoinSpec.Builder("entity")
+ .setNestedSearch("nested", nestedSearchSpec).build();
+
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+ /*queryExpression=*/"",
+ new SearchSpec.Builder().setJoinSpec(joinSpec).build(),
+ /*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)));
+
+ converter.removeInaccessibleSchemaFilter(
+ new CallerAccess(/*callingPackageName=*/"otherPackageName"),
+ visibilityStore,
+ AppSearchTestUtils.createMockVisibilityChecker(
+ /*visiblePrefixedSchemas=*/ ImmutableSet.of(prefix + "schema3")));
+
+ SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
+ assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(prefix + "schema3");
+
+ // Schema 1 and 2 are filtered out of the nested spec. As the JoinSpec has nothing to
+ // search, it should not be part of the SearchSpec.
+ assertThat(searchSpecProto.hasJoinSpec()).isFalse();
}
@Test
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 fc8d058..167ed1c 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
@@ -32,6 +32,7 @@
import androidx.appsearch.app.PackageIdentifier;
import androidx.appsearch.app.VisibilityDocument;
import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.DefaultIcingOptionsConfig;
import androidx.appsearch.localstorage.OptimizeStrategy;
import androidx.appsearch.localstorage.UnlimitedLimitConfig;
import androidx.appsearch.localstorage.util.PrefixUtil;
@@ -127,7 +128,7 @@
// Persist to disk and re-open the AppSearchImpl
appSearchImplInV0.close();
AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ new DefaultIcingOptionsConfig(), /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
VisibilityDocument actualDocument1 = new VisibilityDocument(
@@ -159,6 +160,7 @@
.build();
assertThat(actualDocument1).isEqualTo(expectedDocument1);
assertThat(actualDocument2).isEqualTo(expectedDocument2);
+ appSearchImpl.close();
}
/** Build AppSearchImpl with deprecated visibility schemas version 0. */
@@ -192,7 +194,7 @@
.build();
// Set deprecated visibility schema version 0 into AppSearchImpl.
AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ new DefaultIcingOptionsConfig(), /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
InternalSetSchemaResponse internalSetSchemaResponse = appSearchImpl.setSchema(
VisibilityStore.VISIBILITY_PACKAGE_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 37d8fb6..0ca32c0 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
@@ -27,6 +27,7 @@
import androidx.appsearch.app.SetSchemaRequest;
import androidx.appsearch.app.VisibilityDocument;
import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.DefaultIcingOptionsConfig;
import androidx.appsearch.localstorage.OptimizeStrategy;
import androidx.appsearch.localstorage.UnlimitedLimitConfig;
import androidx.appsearch.localstorage.util.PrefixUtil;
@@ -71,7 +72,7 @@
// Create AppSearchImpl with visibility document version 1;
AppSearchImpl appSearchImplInV1 = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ new DefaultIcingOptionsConfig(), /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
InternalSetSchemaResponse internalSetSchemaResponse = appSearchImplInV1.setSchema(
VisibilityStore.VISIBILITY_PACKAGE_NAME,
@@ -119,7 +120,7 @@
// Persist to disk and re-open the AppSearchImpl
appSearchImplInV1.close();
AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
- /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+ new DefaultIcingOptionsConfig(), /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
VisibilityDocument actualDocument = new VisibilityDocument(
@@ -140,5 +141,6 @@
ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA),
ImmutableSet.of(SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA)));
+ appSearchImpl.close();
}
}
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 0a5364f..99a1ffe 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
@@ -26,6 +26,7 @@
import androidx.appsearch.app.VisibilityDocument;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.DefaultIcingOptionsConfig;
import androidx.appsearch.localstorage.OptimizeStrategy;
import androidx.appsearch.localstorage.UnlimitedLimitConfig;
import androidx.appsearch.localstorage.util.PrefixUtil;
@@ -60,6 +61,7 @@
mAppSearchImpl = AppSearchImpl.create(
mAppSearchDir,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
/*initStatsBuilder=*/ null,
ALWAYS_OPTIMIZE,
/*visibilityChecker=*/null);
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 2eba5ee..cf8c77d 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,8 @@
// fall through
case Features.LIST_FILTER_QUERY_LANGUAGE:
// fall through
+ case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:
+ // fall through
case Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH:
// fall through
case Features.SEARCH_SPEC_PROPERTY_WEIGHTS:
@@ -55,6 +57,10 @@
case Features.TOKENIZER_TYPE_RFC822:
// fall through
case Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION:
+ // fall through
+ case Features.SEARCH_SUGGESTION:
+ // fall through
+ case Features.SCHEMA_SET_DELETION_PROPAGATION:
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 f445af7..a972a58 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
@@ -268,12 +268,13 @@
public static AppSearchImpl create(
@NonNull File icingDir,
@NonNull LimitConfig limitConfig,
+ @NonNull IcingOptionsConfig icingOptionsConfig,
@Nullable InitializeStats.Builder initStatsBuilder,
@NonNull OptimizeStrategy optimizeStrategy,
@Nullable VisibilityChecker visibilityChecker)
throws AppSearchException {
- return new AppSearchImpl(icingDir, limitConfig, initStatsBuilder, optimizeStrategy,
- visibilityChecker);
+ return new AppSearchImpl(icingDir, limitConfig, icingOptionsConfig, initStatsBuilder,
+ optimizeStrategy, visibilityChecker);
}
/**
@@ -282,11 +283,13 @@
private AppSearchImpl(
@NonNull File icingDir,
@NonNull LimitConfig limitConfig,
+ @NonNull IcingOptionsConfig icingOptionsConfig,
@Nullable InitializeStats.Builder initStatsBuilder,
@NonNull OptimizeStrategy optimizeStrategy,
@Nullable VisibilityChecker visibilityChecker)
throws AppSearchException {
Preconditions.checkNotNull(icingDir);
+ Preconditions.checkNotNull(icingOptionsConfig);
mLimitConfig = Preconditions.checkNotNull(limitConfig);
mOptimizeStrategy = Preconditions.checkNotNull(optimizeStrategy);
mVisibilityCheckerLocked = visibilityChecker;
@@ -296,7 +299,15 @@
// We synchronize here because we don't want to call IcingSearchEngine.initialize() more
// than once. It's unnecessary and can be a costly operation.
IcingSearchEngineOptions options = IcingSearchEngineOptions.newBuilder()
- .setBaseDir(icingDir.getAbsolutePath()).build();
+ .setBaseDir(icingDir.getAbsolutePath())
+ .setMaxTokenLength(icingOptionsConfig.getMaxTokenLength())
+ .setIndexMergeSize(icingOptionsConfig.getIndexMergeSize())
+ .setDocumentStoreNamespaceIdFingerprint(
+ icingOptionsConfig.getDocumentStoreNamespaceIdFingerprint())
+ .setOptimizeRebuildIndexThreshold(
+ icingOptionsConfig.getOptimizeRebuildIndexThreshold())
+ .setCompressionLevel(icingOptionsConfig.getCompressionLevel())
+ .build();
LogUtil.piiTrace(TAG, "Constructing IcingSearchEngine, request", options);
mIcingSearchEngineLocked = new IcingSearchEngine(options);
LogUtil.piiTrace(
@@ -1450,7 +1461,7 @@
long rewriteSearchSpecLatencyStartMillis = SystemClock.elapsedRealtime();
SearchSpecProto finalSearchSpec = searchSpecToProtoConverter.toSearchSpecProto();
ResultSpecProto finalResultSpec = searchSpecToProtoConverter.toResultSpecProto(
- mNamespaceMapLocked);
+ mNamespaceMapLocked, mSchemaMapLocked);
ScoringSpecProto scoringSpec = searchSpecToProtoConverter.toScoringSpecProto();
if (sStatsBuilder != null) {
sStatsBuilder.setRewriteSearchSpecLatencyMillis((int)
@@ -1492,6 +1503,11 @@
TAG, "search, response", searchResultProto.getResultsCount(), searchResultProto);
if (sStatsBuilder != null) {
sStatsBuilder.setStatusCode(statusProtoToResultCode(searchResultProto.getStatus()));
+ if (searchSpec.hasJoinSpec()) {
+ // TODO(b/276349029): Log different join types when they get added.
+ sStatsBuilder.setJoinType(AppSearchSchema.StringPropertyConfig
+ .JOINABLE_VALUE_TYPE_QUALIFIED_ID);
+ }
AppSearchLoggerHelper.copyNativeStats(searchResultProto.getQueryStats(), sStatsBuilder);
}
checkSuccess(searchResultProto.getStatus());
@@ -1629,6 +1645,8 @@
if (sStatsBuilder != null) {
sStatsBuilder.setStatusCode(statusProtoToResultCode(searchResultProto.getStatus()));
+ // Join query stats are handled by SearchResultsImpl, which has access to the
+ // original SearchSpec.
AppSearchLoggerHelper.copyNativeStats(searchResultProto.getQueryStats(),
sStatsBuilder);
}
@@ -2218,6 +2236,9 @@
mReadWriteLock.writeLock().lock();
try {
throwIfClosedLocked();
+ if (LogUtil.DEBUG) {
+ Log.d(TAG, "Clear data for package: " + packageName);
+ }
// TODO(b/193494000): We are calling getPackageToDatabases here and in several other
// places within AppSearchImpl. This method is not efficient and does a lot of string
// manipulation. We should find a way to cache the package to database map so it can
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 4772afe..0189673 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
@@ -130,7 +130,10 @@
.setJavaToNativeJniLatencyMillis(
fromNativeStats.getJavaToNativeJniLatencyMs())
.setNativeToJavaJniLatencyMillis(
- fromNativeStats.getNativeToJavaJniLatencyMs());
+ fromNativeStats.getNativeToJavaJniLatencyMs())
+ .setNativeNumJoinedResultsCurrentPage(
+ fromNativeStats.getNumJoinedResultsReturnedCurrentPage())
+ .setNativeJoinLatencyMillis(fromNativeStats.getJoinLatencyMs());
}
/**
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/DefaultIcingOptionsConfig.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/DefaultIcingOptionsConfig.java
new file mode 100644
index 0000000..9c68c38
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/DefaultIcingOptionsConfig.java
@@ -0,0 +1,51 @@
+/*
+ * 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:copyToPath(testing/testutils/src/android/app/appsearch/testutil/external/DefaultIcingOptionsConfig.java)
+package androidx.appsearch.localstorage;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Icing options for AppSearch local-storage. Note, these values are not necessarily the defaults
+ * set in {@link com.google.android.icing.proto.IcingSearchEngineOptions} proto.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class DefaultIcingOptionsConfig implements IcingOptionsConfig {
+ @Override
+ public int getMaxTokenLength() {
+ return DEFAULT_MAX_TOKEN_LENGTH;
+ }
+
+ @Override
+ public int getIndexMergeSize() {
+ return DEFAULT_INDEX_MERGE_SIZE;
+ }
+
+ @Override
+ public boolean getDocumentStoreNamespaceIdFingerprint() {
+ return true;
+ }
+
+ @Override
+ public float getOptimizeRebuildIndexThreshold() {
+ return 0.9f;
+ }
+
+ @Override
+ public int getCompressionLevel() {
+ return DEFAULT_COMPRESSION_LEVEL;
+ }
+}
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
new file mode 100644
index 0000000..28d60c8
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java
@@ -0,0 +1,95 @@
+/*
+ * 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;
+
+import androidx.annotation.RestrictTo;
+
+import com.google.android.icing.proto.IcingSearchEngineOptions;
+
+/**
+ * An interface exposing the optional config flags in {@link IcingSearchEngineOptions} used to
+ * instantiate {@link com.google.android.icing.IcingSearchEngine}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface IcingOptionsConfig {
+ // Defaults from IcingSearchEngineOptions proto
+ int DEFAULT_MAX_TOKEN_LENGTH = 30;
+
+ int DEFAULT_INDEX_MERGE_SIZE = 1048576; // 1 MiB
+
+ boolean DEFAULT_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT = false;
+
+ float DEFAULT_OPTIMIZE_REBUILD_INDEX_THRESHOLD = 0.0f;
+
+ /**
+ * The default compression level in IcingSearchEngineOptions proto matches the
+ * previously-hardcoded document compression level in Icing (which is 3).
+ */
+ int DEFAULT_COMPRESSION_LEVEL = 3;
+
+ /**
+ * The maximum allowable token length. All tokens in excess of this size will be truncated to
+ * max_token_length before being indexed.
+ *
+ * <p>Clients may use this option to prevent unnecessary indexing of long tokens.
+ * Depending on the use case, indexing all of
+ * 'Supercalifragilisticexpialidocious' may be unnecessary - a user is
+ * unlikely to type that entire query. So only indexing the first n bytes may
+ * still provide the desired behavior without wasting resources.
+ */
+ int getMaxTokenLength();
+
+ /**
+ * The size (measured in bytes) at which Icing's internal indices should be
+ * merged. Icing buffers changes together before merging them into a more
+ * compact format. When the buffer exceeds index_merge_size during a Put
+ * operation, the buffer is merged into the larger, more compact index.
+ *
+ * <p>This more compact index is more efficient to search over as the index
+ * grows larger and has smaller system health impact.
+ *
+ * <p>Setting a low index_merge_size increases the frequency of merges -
+ * increasing indexing-time latency and flash wear. Setting a high
+ * index_merge_size leads to larger resource usage and higher query latency.
+ */
+ int getIndexMergeSize();
+
+ /**
+ * Whether to use namespace id or namespace name to build up fingerprint for
+ * document_key_mapper_ and corpus_mapper_ in document store.
+ */
+ boolean getDocumentStoreNamespaceIdFingerprint();
+
+ /**
+ * The threshold of the percentage of invalid documents at which to rebuild index
+ * during optimize.
+ *
+ * <p>We rebuild index if and only if |invalid_documents| / |all_documents| >= threshold.
+ *
+ * <p>Rebuilding the index could be faster than optimizing the index if we have
+ * removed most of the documents. Based on benchmarks, 85%~95% seems to be a good threshold
+ * for most cases.
+ */
+ float getOptimizeRebuildIndexThreshold();
+
+ /**
+ * The level of gzip compression for documents in the Icing document store.
+ *
+ * <p>NO_COMPRESSION = 0, BEST_SPEED = 1, BEST_COMPRESSION = 9
+ */
+ int getCompressionLevel();
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/JetpackOptimizeStrategy.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/JetpackOptimizeStrategy.java
index 6c0ee68..f17b3ce 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/JetpackOptimizeStrategy.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/JetpackOptimizeStrategy.java
@@ -29,7 +29,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class JetpackOptimizeStrategy implements OptimizeStrategy{
+public class JetpackOptimizeStrategy implements OptimizeStrategy {
@VisibleForTesting
static final int DOC_COUNT_OPTIMIZE_THRESHOLD = 1000;
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 b862ce3..758e63b 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
@@ -340,6 +340,7 @@
mAppSearchImpl = AppSearchImpl.create(
icingDir,
new UnlimitedLimitConfig(),
+ new DefaultIcingOptionsConfig(),
initStatsBuilder,
new JetpackOptimizeStrategy(),
/*visibilityChecker=*/null);
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
index 8da40bb..f215cd8 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.SearchResult;
import androidx.appsearch.app.SearchResultPage;
import androidx.appsearch.app.SearchResults;
@@ -114,6 +115,12 @@
searchResultPage = mAppSearchImpl.getNextPage(mPackageName, mNextPageToken,
sStatsBuilder);
if (mLogger != null && sStatsBuilder != null) {
+ // TODO(b/276349029): Log different join types when they get added.
+ if (mSearchSpec.getJoinSpec() != null
+ && !mSearchSpec.getJoinSpec().getChildPropertyExpression().isEmpty()) {
+ sStatsBuilder.setJoinType(AppSearchSchema.StringPropertyConfig
+ .JOINABLE_VALUE_TYPE_QUALIFIED_ID);
+ }
mLogger.logStats(sStatsBuilder.build());
}
}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/UnlimitedLimitConfig.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/UnlimitedLimitConfig.java
index b4f37fa..a402969 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/UnlimitedLimitConfig.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/UnlimitedLimitConfig.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
+// @exportToFramework:copyToPath(testing/testutils/src/android/app/appsearch/testutil/external/UnlimitedLimitConfig.java)
package androidx.appsearch.localstorage;
import androidx.annotation.RestrictTo;
@@ -22,7 +22,6 @@
* In Jetpack, AppSearch doesn't enforce artificial limits on number of documents or size of
* documents, since the app is the only user of the Icing instance. Icing still enforces a docid
* limit of 1M docs.
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class UnlimitedLimitConfig implements LimitConfig {
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 b22d33f..976f0aa 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
@@ -99,6 +99,7 @@
.setValueType(
convertJoinableValueTypeToProto(
stringProperty.getJoinableValueType()))
+ .setPropagateDelete(stringProperty.getDeletionPropagation())
.build();
builder.setJoinableConfig(joinableConfig);
}
@@ -188,6 +189,7 @@
.setJoinableValueType(
convertJoinableValueTypeFromProto(
proto.getJoinableConfig().getValueType()))
+ .setDeletionPropagation(proto.getJoinableConfig().getPropagateDelete())
.setTokenizerType(
proto.getStringIndexingConfig().getTokenizerType().getNumber());
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 0e80ebe..d047745 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
@@ -18,6 +18,7 @@
import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
import static androidx.appsearch.localstorage.util.PrefixUtil.getPackageName;
+import static androidx.appsearch.localstorage.util.PrefixUtil.getPrefix;
import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefix;
import android.util.Log;
@@ -26,6 +27,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.appsearch.app.JoinSpec;
+import androidx.appsearch.app.SearchResult;
import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
@@ -79,6 +81,7 @@
* filters which are stored in AppSearch. This is a field so that we can generate nested protos.
*/
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
@@ -87,6 +90,13 @@
private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMap;
/**
+ * The nested converter, which contains SearchSpec, ResultSpec, and ScoringSpec information
+ * about the nested query. This will remain null if there is no nested {@link JoinSpec}.
+ */
+ @Nullable
+ private SearchSpecToProtoConverter mNestedConverter = null;
+
+ /**
* Creates a {@link SearchSpecToProtoConverter} for given {@link SearchSpec}.
*
* @param queryExpression Query String to search.
@@ -120,11 +130,27 @@
} else {
mTargetPrefixedSchemaFilters = new ArraySet<>();
}
+
+ JoinSpec joinSpec = searchSpec.getJoinSpec();
+ if (joinSpec == null) {
+ return;
+ }
+
+ mNestedConverter = new SearchSpecToProtoConverter(
+ joinSpec.getNestedQuery(),
+ joinSpec.getNestedSearchSpec(),
+ mPrefixes,
+ namespaceMap,
+ schemaMap);
}
/**
* @return 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.
*/
public boolean hasNothingToSearch() {
return mTargetPrefixedNamespaceFilters.isEmpty() || mTargetPrefixedSchemaFilters.isEmpty();
@@ -146,23 +172,68 @@
@NonNull CallerAccess callerAccess,
@Nullable VisibilityStore visibilityStore,
@Nullable VisibilityChecker visibilityChecker) {
+ removeInaccessibleSchemaFilterCached(callerAccess, visibilityStore,
+ /*inaccessibleSchemaPrefixes=*/new ArraySet<>(),
+ /*accessibleSchemaPrefixes=*/new ArraySet<>(), visibilityChecker);
+ }
+
+ /**
+ * For each target schema, we will check visibility store is that accessible to the caller. And
+ * remove this schemas if it is not allowed for caller to query. This private version accepts
+ * two additional parameters to minimize the amount of calls to
+ * {@link VisibilityUtil#isSchemaSearchableByCaller}.
+ *
+ * @param callerAccess Visibility access info of the calling app
+ * @param visibilityStore The {@link VisibilityStore} that store all visibility
+ * information.
+ * @param visibilityChecker Optional visibility checker to check whether the caller
+ * could access target schemas. Pass {@code null} will
+ * reject access for all documents which doesn't belong
+ * to the calling package.
+ * @param inaccessibleSchemaPrefixes A set of schemas that are known to be inaccessible. This
+ * is helpful for reducing duplicate calls to
+ * {@link VisibilityUtil}.
+ * @param accessibleSchemaPrefixes A set of schemas that are known to be accessible. This is
+ * helpful for reducing duplicate calls to
+ * {@link VisibilityUtil}.
+ */
+ private void removeInaccessibleSchemaFilterCached(
+ @NonNull CallerAccess callerAccess,
+ @Nullable VisibilityStore visibilityStore,
+ @NonNull Set<String> inaccessibleSchemaPrefixes,
+ @NonNull Set<String> accessibleSchemaPrefixes,
+ @Nullable VisibilityChecker visibilityChecker) {
Iterator<String> targetPrefixedSchemaFilterIterator =
mTargetPrefixedSchemaFilters.iterator();
while (targetPrefixedSchemaFilterIterator.hasNext()) {
String targetPrefixedSchemaFilter = targetPrefixedSchemaFilterIterator.next();
String packageName = getPackageName(targetPrefixedSchemaFilter);
- if (!VisibilityUtil.isSchemaSearchableByCaller(
+ if (accessibleSchemaPrefixes.contains(targetPrefixedSchemaFilter)) {
+ continue;
+ } else if (inaccessibleSchemaPrefixes.contains(targetPrefixedSchemaFilter)) {
+ targetPrefixedSchemaFilterIterator.remove();
+ } else if (!VisibilityUtil.isSchemaSearchableByCaller(
callerAccess,
packageName,
targetPrefixedSchemaFilter,
visibilityStore,
visibilityChecker)) {
targetPrefixedSchemaFilterIterator.remove();
+ inaccessibleSchemaPrefixes.add(targetPrefixedSchemaFilter);
+ } else {
+ accessibleSchemaPrefixes.add(targetPrefixedSchemaFilter);
}
}
+
+ if (mNestedConverter != null) {
+ mNestedConverter.removeInaccessibleSchemaFilterCached(
+ callerAccess, visibilityStore, inaccessibleSchemaPrefixes,
+ accessibleSchemaPrefixes, visibilityChecker);
+ }
}
+
/** Extracts {@link SearchSpecProto} information from a {@link SearchSpec}. */
@NonNull
public SearchSpecProto toSearchSpecProto() {
@@ -181,25 +252,26 @@
}
protoBuilder.setTermMatchType(termMatchCodeProto);
- JoinSpec joinSpec = mSearchSpec.getJoinSpec();
- if (joinSpec != null) {
- SearchSpecToProtoConverter nestedConverter = new SearchSpecToProtoConverter(
- joinSpec.getNestedQuery(), joinSpec.getNestedSearchSpec(), mPrefixes,
- mNamespaceMap, mSchemaMap);
+ if (mNestedConverter != null && !mNestedConverter.hasNothingToSearch()) {
+ JoinSpecProto.NestedSpecProto nestedSpec =
+ JoinSpecProto.NestedSpecProto.newBuilder()
+ .setResultSpec(mNestedConverter.toResultSpecProto(
+ mNamespaceMap, mSchemaMap))
+ .setScoringSpec(mNestedConverter.toScoringSpecProto())
+ .setSearchSpec(mNestedConverter.toSearchSpecProto())
+ .build();
- JoinSpecProto.NestedSpecProto nestedSpec = JoinSpecProto.NestedSpecProto.newBuilder()
- .setResultSpec(nestedConverter.toResultSpecProto(mNamespaceMap))
- .setScoringSpec(nestedConverter.toScoringSpecProto())
- .setSearchSpec(nestedConverter.toSearchSpecProto())
- .build();
-
- JoinSpecProto.Builder joinSpecProtoBuilder = JoinSpecProto.newBuilder()
- .setNestedSpec(nestedSpec)
- .setParentPropertyExpression(JoinSpec.QUALIFIED_ID)
- .setChildPropertyExpression(joinSpec.getChildPropertyExpression())
- .setAggregationScoringStrategy(
- toAggregationScoringStrategy(joinSpec.getAggregationScoringStrategy()))
- .setMaxJoinedChildCount(joinSpec.getMaxJoinedResultCount());
+ // This cannot be null, otherwise mNestedConverter would be null as well.
+ JoinSpec joinSpec = mSearchSpec.getJoinSpec();
+ JoinSpecProto.Builder joinSpecProtoBuilder =
+ JoinSpecProto.newBuilder()
+ .setNestedSpec(nestedSpec)
+ .setParentPropertyExpression(JoinSpec.QUALIFIED_ID)
+ .setChildPropertyExpression(joinSpec.getChildPropertyExpression())
+ .setAggregationScoringStrategy(
+ toAggregationScoringStrategy(
+ joinSpec.getAggregationScoringStrategy()))
+ .setMaxJoinedChildCount(joinSpec.getMaxJoinedResultCount());
protoBuilder.setJoinSpec(joinSpecProtoBuilder);
}
@@ -252,10 +324,14 @@
*
* @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.
*/
@NonNull
public ResultSpecProto toResultSpecProto(
- @NonNull Map<String, Set<String>> namespaceMap) {
+ @NonNull Map<String, Set<String>> namespaceMap,
+ @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
ResultSpecProto.Builder resultSpecBuilder = ResultSpecProto.newBuilder()
.setNumPerPage(mSearchSpec.getResultCountPerPage())
.setSnippetSpec(
@@ -279,12 +355,36 @@
namespaceMap, resultSpecBuilder);
resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE;
break;
+ case SearchSpec.GROUPING_TYPE_PER_SCHEMA:
+ addPerSchemaResultGrouping(mPrefixes, mSearchSpec.getResultGroupingLimit(),
+ schemaMap, resultSpecBuilder);
+ resultGroupingType = ResultSpecProto.ResultGroupingType.SCHEMA_TYPE;
+ break;
case SearchSpec.GROUPING_TYPE_PER_PACKAGE | SearchSpec.GROUPING_TYPE_PER_NAMESPACE:
addPerPackagePerNamespaceResultGroupings(mPrefixes,
mSearchSpec.getResultGroupingLimit(),
namespaceMap, resultSpecBuilder);
resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE;
break;
+ case SearchSpec.GROUPING_TYPE_PER_PACKAGE | SearchSpec.GROUPING_TYPE_PER_SCHEMA:
+ addPerPackagePerSchemaResultGroupings(mPrefixes,
+ mSearchSpec.getResultGroupingLimit(),
+ schemaMap, resultSpecBuilder);
+ resultGroupingType = ResultSpecProto.ResultGroupingType.SCHEMA_TYPE;
+ break;
+ case SearchSpec.GROUPING_TYPE_PER_NAMESPACE | SearchSpec.GROUPING_TYPE_PER_SCHEMA:
+ addPerNamespaceAndSchemaResultGrouping(mPrefixes,
+ mSearchSpec.getResultGroupingLimit(),
+ namespaceMap, schemaMap, 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(mPrefixes,
+ mSearchSpec.getResultGroupingLimit(),
+ namespaceMap, schemaMap, resultSpecBuilder);
+ resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE_AND_SCHEMA_TYPE;
+ break;
default:
break;
}
@@ -363,21 +463,54 @@
}
/**
- * Adds result groupings for each namespace in each package being queried for.
+ * 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.
*
- * @param prefixes Prefixes that we should prepend to all our filters
- * @param maxNumResults The maximum number of results for each grouping to support.
+ * @param prefixes Prefixes that we should prepend to all our filters.
* @param namespaceMap The namespace map contains all prefixed existing namespaces.
- * @param resultSpecBuilder ResultSpecs as specified by client
*/
- private static void addPerPackagePerNamespaceResultGroupings(
+ private static Map<String, List<String>> getNamespaceToPrefixedNamespaces(
@NonNull Set<String> prefixes,
- int maxNumResults,
- @NonNull Map<String, Set<String>> namespaceMap,
- @NonNull ResultSpecProto.Builder resultSpecBuilder) {
- // Create a map for package+namespace to prefixedNamespaces. This is NOT necessarily the
- // same as the list of namespaces. If one package has multiple databases, each with the same
- // namespace, then those should be grouped together.
+ @NonNull Map<String, Set<String>> namespaceMap) {
+ Map<String, List<String>> namespaceToPrefixedNamespaces = new ArrayMap<>();
+ for (String prefix : prefixes) {
+ Set<String> prefixedNamespaces = namespaceMap.get(prefix);
+ if (prefixedNamespaces == null) {
+ continue;
+ }
+ for (String prefixedNamespace : prefixedNamespaces) {
+ String namespace;
+ try {
+ namespace = removePrefix(prefixedNamespace);
+ } catch (AppSearchException e) {
+ // This should never happen. Skip this namespace if it does.
+ Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
+ continue;
+ }
+ List<String> groupedPrefixedNamespaces =
+ namespaceToPrefixedNamespaces.get(namespace);
+ if (groupedPrefixedNamespaces == null) {
+ groupedPrefixedNamespaces = new ArrayList<>();
+ namespaceToPrefixedNamespaces.put(namespace, groupedPrefixedNamespaces);
+ }
+ groupedPrefixedNamespaces.add(prefixedNamespace);
+ }
+ }
+ return namespaceToPrefixedNamespaces;
+ }
+
+ /**
+ * Returns a map for package+namespace to prefixedNamespaces. This is NOT necessarily the
+ * same as the list of namespaces. If one package has multiple databases, each with the same
+ * namespace, then those should be grouped together.
+ *
+ * @param prefixes Prefixes that we should prepend to all our filters.
+ * @param namespaceMap The namespace map contains all prefixed existing namespaces.
+ */
+ private static Map<String, List<String>> getPackageAndNamespaceToPrefixedNamespaces(
+ @NonNull Set<String> prefixes,
+ @NonNull Map<String, Set<String>> namespaceMap) {
Map<String, List<String>> packageAndNamespaceToNamespaces = new ArrayMap<>();
for (String prefix : prefixes) {
Set<String> prefixedNamespaces = namespaceMap.get(prefix);
@@ -408,14 +541,113 @@
namespaceList.add(prefixedNamespace);
}
}
+ return packageAndNamespaceToNamespaces;
+ }
+
+ /**
+ * Returns a map of schema to prefixedSchemas. This is NOT necessarily the
+ * same as the list of schemas. If a schema exists under different packages and/or
+ * 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.
+ */
+ private static Map<String, List<String>> getSchemaToPrefixedSchemas(
+ @NonNull Set<String> prefixes,
+ @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+ Map<String, List<String>> schemaToPrefixedSchemas = new ArrayMap<>();
+ for (String prefix : prefixes) {
+ Map<String, SchemaTypeConfigProto> prefixedSchemas = schemaMap.get(prefix);
+ if (prefixedSchemas == null) {
+ continue;
+ }
+ for (String prefixedSchema : prefixedSchemas.keySet()) {
+ String schema;
+ try {
+ schema = removePrefix(prefixedSchema);
+ } catch (AppSearchException e) {
+ // This should never happen. Skip this schema if it does.
+ Log.e(TAG, "Prefixed schema " + prefixedSchema + " is malformed.");
+ continue;
+ }
+ List<String> groupedPrefixedSchemas =
+ schemaToPrefixedSchemas.get(schema);
+ if (groupedPrefixedSchemas == null) {
+ groupedPrefixedSchemas = new ArrayList<>();
+ schemaToPrefixedSchemas.put(schema, groupedPrefixedSchemas);
+ }
+ groupedPrefixedSchemas.add(prefixedSchema);
+ }
+ }
+ return schemaToPrefixedSchemas;
+ }
+
+ /**
+ * Returns a map for package+schema to prefixedSchemas. This is NOT necessarily the
+ * same as the list of schemas. If one package has multiple databases, each with the same
+ * 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.
+ */
+ private static Map<String, List<String>> getPackageAndSchemaToPrefixedSchemas(
+ @NonNull Set<String> prefixes,
+ @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+ Map<String, List<String>> packageAndSchemaToSchemas = new ArrayMap<>();
+ for (String prefix : prefixes) {
+ Map<String, SchemaTypeConfigProto> prefixedSchemas = schemaMap.get(prefix);
+ if (prefixedSchemas == null) {
+ continue;
+ }
+ 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.
+ String emptyDatabasePrefix = createPrefix(packageName, /*database*/"");
+ for (String prefixedSchema : prefixedSchemas.keySet()) {
+ String schema;
+ try {
+ schema = removePrefix(prefixedSchema);
+ } catch (AppSearchException e) {
+ // This should never happen. Skip this schema if it does.
+ Log.e(TAG, "Prefixed schema " + prefixedSchema + " is malformed.");
+ continue;
+ }
+ String emptyDatabasePrefixedSchema = emptyDatabasePrefix + schema;
+ List<String> schemaList =
+ packageAndSchemaToSchemas.get(emptyDatabasePrefixedSchema);
+ if (schemaList == null) {
+ schemaList = new ArrayList<>();
+ packageAndSchemaToSchemas.put(emptyDatabasePrefixedSchema, schemaList);
+ }
+ schemaList.add(prefixedSchema);
+ }
+ }
+ return packageAndSchemaToSchemas;
+ }
+
+ /**
+ * Adds result groupings for each namespace in each package being queried for.
+ *
+ * @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 resultSpecBuilder ResultSpecs as specified by client
+ */
+ private static void addPerPackagePerNamespaceResultGroupings(
+ @NonNull Set<String> prefixes,
+ int maxNumResults,
+ @NonNull Map<String, Set<String>> namespaceMap,
+ @NonNull ResultSpecProto.Builder resultSpecBuilder) {
+ Map<String, List<String>> packageAndNamespaceToNamespaces =
+ getPackageAndNamespaceToPrefixedNamespaces(prefixes, namespaceMap);
for (List<String> prefixedNamespaces : packageAndNamespaceToNamespaces.values()) {
List<ResultSpecProto.ResultGrouping.Entry> entries =
new ArrayList<>(prefixedNamespaces.size());
- for (String namespace : prefixedNamespaces) {
+ for (int i = 0; i < prefixedNamespaces.size(); i++) {
entries.add(
ResultSpecProto.ResultGrouping.Entry.newBuilder()
- .setNamespace(namespace).build());
+ .setNamespace(prefixedNamespaces.get(i)).build());
}
resultSpecBuilder.addResultGroupings(
ResultSpecProto.ResultGrouping.newBuilder()
@@ -424,6 +656,84 @@
}
/**
+ * Adds result groupings for each schema type in each package being queried for.
+ *
+ * @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 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 ResultSpecProto.Builder resultSpecBuilder) {
+ Map<String, List<String>> packageAndSchemaToSchemas =
+ getPackageAndSchemaToPrefixedSchemas(prefixes, schemaMap);
+
+ for (List<String> prefixedSchemas : packageAndSchemaToSchemas.values()) {
+ List<ResultSpecProto.ResultGrouping.Entry> entries =
+ new ArrayList<>(prefixedSchemas.size());
+ for (int i = 0; i < prefixedSchemas.size(); i++) {
+ entries.add(
+ ResultSpecProto.ResultGrouping.Entry.newBuilder()
+ .setSchema(prefixedSchemas.get(i)).build());
+ }
+ resultSpecBuilder.addResultGroupings(
+ ResultSpecProto.ResultGrouping.newBuilder()
+ .addAllEntryGroupings(entries).setMaxResults(maxNumResults));
+ }
+ }
+
+ /**
+ * Adds result groupings for each namespace and schema type being queried for.
+ *
+ * @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 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 ResultSpecProto.Builder resultSpecBuilder) {
+ Map<String, List<String>> packageAndNamespaceToNamespaces =
+ getPackageAndNamespaceToPrefixedNamespaces(prefixes, namespaceMap);
+ Map<String, List<String>> packageAndSchemaToSchemas =
+ getPackageAndSchemaToPrefixedSchemas(prefixes, schemaMap);
+
+ for (List<String> prefixedNamespaces : packageAndNamespaceToNamespaces.values()) {
+ for (List<String> prefixedSchemas : packageAndSchemaToSchemas.values()) {
+ List<ResultSpecProto.ResultGrouping.Entry> entries =
+ new ArrayList<>(prefixedNamespaces.size() * prefixedSchemas.size());
+ // Iterate through all namespaces.
+ for (int i = 0; i < prefixedNamespaces.size(); i++) {
+ String namespacePackage = getPackageName(prefixedNamespaces.get(i));
+ // Iterate through all schemas.
+ for (int j = 0; j < prefixedSchemas.size(); j++) {
+ String schemaPackage = getPackageName(prefixedSchemas.get(j));
+ if (namespacePackage.equals(schemaPackage)) {
+ entries.add(
+ ResultSpecProto.ResultGrouping.Entry.newBuilder()
+ .setNamespace(prefixedNamespaces.get(i))
+ .setSchema(prefixedSchemas.get(j))
+ .build());
+ }
+ }
+ }
+ if (entries.size() > 0) {
+ resultSpecBuilder.addResultGroupings(
+ ResultSpecProto.ResultGrouping.newBuilder()
+ .addAllEntryGroupings(entries).setMaxResults(maxNumResults));
+ }
+ }
+ }
+ }
+
+ /**
* Adds result groupings for each package being queried for.
*
* @param prefixes Prefixes that we should prepend to all our filters
@@ -479,42 +789,16 @@
int maxNumResults,
@NonNull Map<String, Set<String>> namespaceMap,
@NonNull ResultSpecProto.Builder resultSpecBuilder) {
- // Create 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.
- Map<String, List<String>> namespaceToPrefixedNamespaces = new ArrayMap<>();
- for (String prefix : prefixes) {
- Set<String> prefixedNamespaces = namespaceMap.get(prefix);
- if (prefixedNamespaces == null) {
- continue;
- }
- for (String prefixedNamespace : prefixedNamespaces) {
- String namespace;
- try {
- namespace = removePrefix(prefixedNamespace);
- } catch (AppSearchException e) {
- // This should never happen. Skip this namespace if it does.
- Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
- continue;
- }
- List<String> groupedPrefixedNamespaces =
- namespaceToPrefixedNamespaces.get(namespace);
- if (groupedPrefixedNamespaces == null) {
- groupedPrefixedNamespaces = new ArrayList<>();
- namespaceToPrefixedNamespaces.put(namespace,
- groupedPrefixedNamespaces);
- }
- groupedPrefixedNamespaces.add(prefixedNamespace);
- }
- }
+ Map<String, List<String>> namespaceToPrefixedNamespaces =
+ getNamespaceToPrefixedNamespaces(prefixes, namespaceMap);
for (List<String> prefixedNamespaces : namespaceToPrefixedNamespaces.values()) {
List<ResultSpecProto.ResultGrouping.Entry> entries =
new ArrayList<>(prefixedNamespaces.size());
- for (String namespace : prefixedNamespaces) {
+ for (int i = 0; i < prefixedNamespaces.size(); i++) {
entries.add(
ResultSpecProto.ResultGrouping.Entry.newBuilder()
- .setNamespace(namespace).build());
+ .setNamespace(prefixedNamespaces.get(i)).build());
}
resultSpecBuilder.addResultGroupings(
ResultSpecProto.ResultGrouping.newBuilder()
@@ -523,6 +807,90 @@
}
/**
+ * Adds result groupings for each schema type being queried for.
+ *
+ * @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 resultSpecBuilder ResultSpec as specified by client.
+ */
+ private static void addPerSchemaResultGrouping(
+ @NonNull Set<String> prefixes,
+ int maxNumResults,
+ @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+ @NonNull ResultSpecProto.Builder resultSpecBuilder) {
+ Map<String, List<String>> schemaToPrefixedSchemas =
+ getSchemaToPrefixedSchemas(prefixes, schemaMap);
+
+ for (List<String> prefixedSchemas : schemaToPrefixedSchemas.values()) {
+ List<ResultSpecProto.ResultGrouping.Entry> entries =
+ new ArrayList<>(prefixedSchemas.size());
+ for (int i = 0; i < prefixedSchemas.size(); i++) {
+ entries.add(
+ ResultSpecProto.ResultGrouping.Entry.newBuilder()
+ .setSchema(prefixedSchemas.get(i)).build());
+ }
+ resultSpecBuilder.addResultGroupings(
+ ResultSpecProto.ResultGrouping.newBuilder()
+ .addAllEntryGroupings(entries).setMaxResults(maxNumResults));
+ }
+ }
+
+ /**
+ * Adds result groupings for each namespace and schema type being queried for.
+ *
+ * @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 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 ResultSpecProto.Builder resultSpecBuilder) {
+ Map<String, List<String>> namespaceToPrefixedNamespaces =
+ getNamespaceToPrefixedNamespaces(prefixes, namespaceMap);
+ Map<String, List<String>> schemaToPrefixedSchemas =
+ getSchemaToPrefixedSchemas(prefixes, schemaMap);
+
+ for (List<String> prefixedNamespaces : namespaceToPrefixedNamespaces.values()) {
+ for (List<String> prefixedSchemas : schemaToPrefixedSchemas.values()) {
+ List<ResultSpecProto.ResultGrouping.Entry> entries =
+ new ArrayList<>(prefixedNamespaces.size() * prefixedSchemas.size());
+ // Iterate through all namespaces.
+ for (int i = 0; i < prefixedNamespaces.size(); i++) {
+ // Iterate through all schemas.
+ for (int j = 0; j < prefixedSchemas.size(); j++) {
+ try {
+ if (getPrefix(prefixedNamespaces.get(i))
+ .equals(getPrefix(prefixedSchemas.get(j)))) {
+ entries.add(
+ ResultSpecProto.ResultGrouping.Entry.newBuilder()
+ .setNamespace(prefixedNamespaces.get(i))
+ .setSchema(prefixedSchemas.get(j))
+ .build());
+ }
+ } catch (AppSearchException e) {
+ // This should never happen. Skip this schema if it does.
+ Log.e(TAG, "Prefixed string " + prefixedNamespaces.get(i) + " or "
+ + prefixedSchemas.get(j) + " is malformed.");
+ continue;
+ }
+ }
+ }
+ if (entries.size() > 0) {
+ resultSpecBuilder.addResultGroupings(
+ ResultSpecProto.ResultGrouping.newBuilder()
+ .addAllEntryGroupings(entries).setMaxResults(maxNumResults));
+ }
+ }
+ }
+ }
+
+ /**
* Adds {@link TypePropertyWeights} to {@link ScoringSpecProto}.
*
* <p>{@link TypePropertyWeights} are added to the {@link ScoringSpecProto} with database and
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 cb29317..02d2971 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
@@ -20,11 +20,16 @@
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.AppSearchResult;
+import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Set;
/**
* A class for setting basic information to log for all function calls.
@@ -36,7 +41,7 @@
* However, {@link CallStats} can still be used along with the detailed stats class for easy
* aggregation/analysis with other function calls.
*
- * @hide
+ * <!--@exportToFramework:hide-->
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class CallStats {
@@ -58,6 +63,20 @@
CALL_TYPE_REMOVE_DOCUMENT_BY_SEARCH,
CALL_TYPE_GLOBAL_GET_DOCUMENT_BY_ID,
CALL_TYPE_SCHEMA_MIGRATION,
+ CALL_TYPE_GLOBAL_GET_SCHEMA,
+ CALL_TYPE_GET_SCHEMA,
+ CALL_TYPE_GET_NAMESPACES,
+ CALL_TYPE_GET_NEXT_PAGE,
+ CALL_TYPE_INVALIDATE_NEXT_PAGE_TOKEN,
+ CALL_TYPE_WRITE_SEARCH_RESULTS_TO_FILE,
+ CALL_TYPE_PUT_DOCUMENTS_FROM_FILE,
+ CALL_TYPE_SEARCH_SUGGESTION,
+ CALL_TYPE_REPORT_SYSTEM_USAGE,
+ CALL_TYPE_REPORT_USAGE,
+ CALL_TYPE_GET_STORAGE_INFO,
+ CALL_TYPE_REGISTER_OBSERVER_CALLBACK,
+ CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK,
+ CALL_TYPE_GLOBAL_GET_NEXT_PAGE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface CallType {
@@ -80,6 +99,51 @@
public static final int CALL_TYPE_REMOVE_DOCUMENT_BY_SEARCH = 14;
public static final int CALL_TYPE_GLOBAL_GET_DOCUMENT_BY_ID = 15;
public static final int CALL_TYPE_SCHEMA_MIGRATION = 16;
+ public static final int CALL_TYPE_GLOBAL_GET_SCHEMA = 17;
+ public static final int CALL_TYPE_GET_SCHEMA = 18;
+ public static final int CALL_TYPE_GET_NAMESPACES = 19;
+ public static final int CALL_TYPE_GET_NEXT_PAGE = 20;
+ public static final int CALL_TYPE_INVALIDATE_NEXT_PAGE_TOKEN = 21;
+ public static final int CALL_TYPE_WRITE_SEARCH_RESULTS_TO_FILE = 22;
+ public static final int CALL_TYPE_PUT_DOCUMENTS_FROM_FILE = 23;
+ public static final int CALL_TYPE_SEARCH_SUGGESTION = 24;
+ public static final int CALL_TYPE_REPORT_SYSTEM_USAGE = 25;
+ public static final int CALL_TYPE_REPORT_USAGE = 26;
+ public static final int CALL_TYPE_GET_STORAGE_INFO = 27;
+ 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;
+
+ // These strings are for the subset of call types that correspond to an AppSearchManager API
+ private static final String CALL_TYPE_STRING_INITIALIZE = "initialize";
+ private static final String CALL_TYPE_STRING_SET_SCHEMA = "localSetSchema";
+ private static final String CALL_TYPE_STRING_PUT_DOCUMENTS = "localPutDocuments";
+ private static final String CALL_TYPE_STRING_GET_DOCUMENTS = "localGetDocuments";
+ private static final String CALL_TYPE_STRING_REMOVE_DOCUMENTS_BY_ID = "localRemoveByDocumentId";
+ private static final String CALL_TYPE_STRING_SEARCH = "localSearch";
+ private static final String CALL_TYPE_STRING_FLUSH = "flush";
+ private static final String CALL_TYPE_STRING_GLOBAL_SEARCH = "globalSearch";
+ private static final String CALL_TYPE_STRING_REMOVE_DOCUMENTS_BY_SEARCH = "localRemoveBySearch";
+ private static final String CALL_TYPE_STRING_GLOBAL_GET_DOCUMENT_BY_ID = "globalGetDocuments";
+ private static final String CALL_TYPE_STRING_GLOBAL_GET_SCHEMA = "globalGetSchema";
+ private static final String CALL_TYPE_STRING_GET_SCHEMA = "localGetSchema";
+ private static final String CALL_TYPE_STRING_GET_NAMESPACES = "localGetNamespaces";
+ private static final String CALL_TYPE_STRING_GET_NEXT_PAGE = "localGetNextPage";
+ private static final String CALL_TYPE_STRING_INVALIDATE_NEXT_PAGE_TOKEN =
+ "invalidateNextPageToken";
+ private static final String CALL_TYPE_STRING_WRITE_SEARCH_RESULTS_TO_FILE =
+ "localWriteSearchResultsToFile";
+ private static final String CALL_TYPE_STRING_PUT_DOCUMENTS_FROM_FILE =
+ "localPutDocumentsFromFile";
+ private static final String CALL_TYPE_STRING_SEARCH_SUGGESTION = "localSearchSuggestion";
+ private static final String CALL_TYPE_STRING_REPORT_SYSTEM_USAGE = "globalReportUsage";
+ private static final String CALL_TYPE_STRING_REPORT_USAGE = "localReportUsage";
+ private static final String CALL_TYPE_STRING_GET_STORAGE_INFO = "localGetStorageInfo";
+ private static final String CALL_TYPE_STRING_REGISTER_OBSERVER_CALLBACK =
+ "globalRegisterObserverCallback";
+ private static final String CALL_TYPE_STRING_UNREGISTER_OBSERVER_CALLBACK =
+ "globalUnregisterObserverCallback";
+ private static final String CALL_TYPE_STRING_GLOBAL_GET_NEXT_PAGE = "globalGetNextPage";
@Nullable
private final String mPackageName;
@@ -149,8 +213,9 @@
* Returns number of operations succeeded.
*
* <p>For example, for
- * {@link androidx.appsearch.app.AppSearchSession#putAsync}, it is the total number of individual
- * successful put operations. In this case, how many documents are successfully indexed.
+ * {@link androidx.appsearch.app.AppSearchSession#putAsync}, it is the total number of
+ * individual successful put operations. In this case, how many documents are successfully
+ * indexed.
*
* <p>For non-batch calls such as
* {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync}, the sum of
@@ -166,8 +231,8 @@
* Returns number of operations failed.
*
* <p>For example, for
- * {@link androidx.appsearch.app.AppSearchSession#putAsync}, it is the total number of individual
- * failed put operations. In this case, how many documents are failed to be indexed.
+ * {@link androidx.appsearch.app.AppSearchSession#putAsync}, it is the total number of
+ * individual failed put operations. In this case, how many documents are failed to be indexed.
*
* <p>For non-batch calls such as
* {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync}, the sum of
@@ -195,20 +260,23 @@
int mNumOperationsFailed;
/** Sets the PackageName used by the session. */
+ @CanIgnoreReturnValue
@NonNull
- public Builder setPackageName(@NonNull String packageName) {
- mPackageName = Preconditions.checkNotNull(packageName);
+ public Builder setPackageName(@Nullable String packageName) {
+ mPackageName = packageName;
return this;
}
/** Sets the database used by the session. */
+ @CanIgnoreReturnValue
@NonNull
- public Builder setDatabase(@NonNull String database) {
- mDatabase = Preconditions.checkNotNull(database);
+ public Builder setDatabase(@Nullable String database) {
+ mDatabase = database;
return this;
}
/** Sets the status code. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
mStatusCode = statusCode;
@@ -216,6 +284,7 @@
}
/** Sets total latency in millis. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setTotalLatencyMillis(int totalLatencyMillis) {
mTotalLatencyMillis = totalLatencyMillis;
@@ -223,6 +292,7 @@
}
/** Sets type of the call. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setCallType(@CallType int callType) {
mCallType = callType;
@@ -230,6 +300,7 @@
}
/** Sets estimated binder latency, in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setEstimatedBinderLatencyMillis(int estimatedBinderLatencyMillis) {
mEstimatedBinderLatencyMillis = estimatedBinderLatencyMillis;
@@ -250,6 +321,7 @@
* {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
* operation.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setNumOperationsSucceeded(int numOperationsSucceeded) {
mNumOperationsSucceeded = numOperationsSucceeded;
@@ -269,6 +341,7 @@
* {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
* operation.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setNumOperationsFailed(int numOperationsFailed) {
mNumOperationsFailed = numOperationsFailed;
@@ -281,4 +354,97 @@
return new CallStats(/* builder= */ this);
}
}
+
+ /**
+ * Returns the {@link CallStats.CallType} represented by the given AppSearchManager API name. If
+ * an unknown name is provided, {@link CallStats.CallType#CALL_TYPE_UNKNOWN} is returned.
+ */
+ @CallType
+ public static int getApiCallTypeFromName(@NonNull String name) {
+ switch (name) {
+ case CALL_TYPE_STRING_INITIALIZE:
+ return CALL_TYPE_INITIALIZE;
+ case CALL_TYPE_STRING_SET_SCHEMA:
+ return CALL_TYPE_SET_SCHEMA;
+ case CALL_TYPE_STRING_PUT_DOCUMENTS:
+ return CALL_TYPE_PUT_DOCUMENTS;
+ case CALL_TYPE_STRING_GET_DOCUMENTS:
+ return CALL_TYPE_GET_DOCUMENTS;
+ case CALL_TYPE_STRING_REMOVE_DOCUMENTS_BY_ID:
+ return CALL_TYPE_REMOVE_DOCUMENTS_BY_ID;
+ case CALL_TYPE_STRING_SEARCH:
+ return CALL_TYPE_SEARCH;
+ case CALL_TYPE_STRING_FLUSH:
+ return CALL_TYPE_FLUSH;
+ case CALL_TYPE_STRING_GLOBAL_SEARCH:
+ return CALL_TYPE_GLOBAL_SEARCH;
+ case CALL_TYPE_STRING_REMOVE_DOCUMENTS_BY_SEARCH:
+ return CALL_TYPE_REMOVE_DOCUMENTS_BY_SEARCH;
+ case CALL_TYPE_STRING_GLOBAL_GET_DOCUMENT_BY_ID:
+ return CALL_TYPE_GLOBAL_GET_DOCUMENT_BY_ID;
+ case CALL_TYPE_STRING_GLOBAL_GET_SCHEMA:
+ return CALL_TYPE_GLOBAL_GET_SCHEMA;
+ case CALL_TYPE_STRING_GET_SCHEMA:
+ return CALL_TYPE_GET_SCHEMA;
+ case CALL_TYPE_STRING_GET_NAMESPACES:
+ return CALL_TYPE_GET_NAMESPACES;
+ case CALL_TYPE_STRING_GET_NEXT_PAGE:
+ return CALL_TYPE_GET_NEXT_PAGE;
+ case CALL_TYPE_STRING_INVALIDATE_NEXT_PAGE_TOKEN:
+ return CALL_TYPE_INVALIDATE_NEXT_PAGE_TOKEN;
+ case CALL_TYPE_STRING_WRITE_SEARCH_RESULTS_TO_FILE:
+ return CALL_TYPE_WRITE_SEARCH_RESULTS_TO_FILE;
+ case CALL_TYPE_STRING_PUT_DOCUMENTS_FROM_FILE:
+ return CALL_TYPE_PUT_DOCUMENTS_FROM_FILE;
+ case CALL_TYPE_STRING_SEARCH_SUGGESTION:
+ return CALL_TYPE_SEARCH_SUGGESTION;
+ case CALL_TYPE_STRING_REPORT_SYSTEM_USAGE:
+ return CALL_TYPE_REPORT_SYSTEM_USAGE;
+ case CALL_TYPE_STRING_REPORT_USAGE:
+ return CALL_TYPE_REPORT_USAGE;
+ case CALL_TYPE_STRING_GET_STORAGE_INFO:
+ return CALL_TYPE_GET_STORAGE_INFO;
+ case CALL_TYPE_STRING_REGISTER_OBSERVER_CALLBACK:
+ return CALL_TYPE_REGISTER_OBSERVER_CALLBACK;
+ case CALL_TYPE_STRING_UNREGISTER_OBSERVER_CALLBACK:
+ return CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK;
+ case CALL_TYPE_STRING_GLOBAL_GET_NEXT_PAGE:
+ return CALL_TYPE_GLOBAL_GET_NEXT_PAGE;
+ default:
+ return CALL_TYPE_UNKNOWN;
+ }
+ }
+
+ /**
+ * Returns the set of all {@link CallStats.CallType} that map to an AppSearchManager API.
+ */
+ @VisibleForTesting
+ @NonNull
+ public static Set<Integer> getAllApiCallTypes() {
+ return new ArraySet<>(Arrays.asList(
+ CALL_TYPE_INITIALIZE,
+ CALL_TYPE_SET_SCHEMA,
+ CALL_TYPE_PUT_DOCUMENTS,
+ CALL_TYPE_GET_DOCUMENTS,
+ CALL_TYPE_REMOVE_DOCUMENTS_BY_ID,
+ CALL_TYPE_SEARCH,
+ CALL_TYPE_FLUSH,
+ CALL_TYPE_GLOBAL_SEARCH,
+ CALL_TYPE_REMOVE_DOCUMENTS_BY_SEARCH,
+ CALL_TYPE_GLOBAL_GET_DOCUMENT_BY_ID,
+ CALL_TYPE_GLOBAL_GET_SCHEMA,
+ CALL_TYPE_GET_SCHEMA,
+ CALL_TYPE_GET_NAMESPACES,
+ CALL_TYPE_GET_NEXT_PAGE,
+ CALL_TYPE_INVALIDATE_NEXT_PAGE_TOKEN,
+ CALL_TYPE_WRITE_SEARCH_RESULTS_TO_FILE,
+ CALL_TYPE_PUT_DOCUMENTS_FROM_FILE,
+ CALL_TYPE_SEARCH_SUGGESTION,
+ CALL_TYPE_REPORT_SYSTEM_USAGE,
+ CALL_TYPE_REPORT_USAGE,
+ CALL_TYPE_GET_STORAGE_INFO,
+ CALL_TYPE_REGISTER_OBSERVER_CALLBACK,
+ CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK,
+ CALL_TYPE_GLOBAL_GET_NEXT_PAGE));
+ }
}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/InitializeStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/InitializeStats.java
index f88d257..6effe35 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/InitializeStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/InitializeStats.java
@@ -19,6 +19,7 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.app.AppSearchResult;
import androidx.core.util.Preconditions;
@@ -288,6 +289,7 @@
int mResetStatusCode;
/** Sets the status of the initialization. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
mStatusCode = statusCode;
@@ -295,6 +297,7 @@
}
/** Sets the total latency of the initialization in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setTotalLatencyMillis(int totalLatencyMillis) {
mTotalLatencyMillis = totalLatencyMillis;
@@ -307,6 +310,7 @@
* <p>If there is a deSync, it means AppSearch and IcingSearchEngine have an inconsistent
* view of what data should exist.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setHasDeSync(boolean hasDeSync) {
mHasDeSync = hasDeSync;
@@ -314,6 +318,7 @@
}
/** Sets time used to read and process the schema and namespaces. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setPrepareSchemaAndNamespacesLatencyMillis(
int prepareSchemaAndNamespacesLatencyMillis) {
@@ -322,6 +327,7 @@
}
/** Sets time used to read and process the visibility file. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setPrepareVisibilityStoreLatencyMillis(
int prepareVisibilityStoreLatencyMillis) {
@@ -330,6 +336,7 @@
}
/** Sets overall time used for the native function call. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setNativeLatencyMillis(int nativeLatencyMillis) {
mNativeLatencyMillis = nativeLatencyMillis;
@@ -344,6 +351,7 @@
* <li> {@link InitializeStats#RECOVERY_CAUSE_TOTAL_CHECKSUM_MISMATCH}
* <li> {@link InitializeStats#RECOVERY_CAUSE_IO_ERROR}
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setDocumentStoreRecoveryCause(
@RecoveryCause int documentStoreRecoveryCause) {
@@ -358,6 +366,7 @@
* <li> {@link InitializeStats#RECOVERY_CAUSE_TOTAL_CHECKSUM_MISMATCH}
* <li> {@link InitializeStats#RECOVERY_CAUSE_IO_ERROR}
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setIndexRestorationCause(
@RecoveryCause int indexRestorationCause) {
@@ -370,6 +379,7 @@
* <p> Possible causes:
* <li> {@link InitializeStats#RECOVERY_CAUSE_IO_ERROR}
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setSchemaStoreRecoveryCause(
@RecoveryCause int schemaStoreRecoveryCause) {
@@ -378,6 +388,7 @@
}
/** Sets time used to recover the document store. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setDocumentStoreRecoveryLatencyMillis(
int documentStoreRecoveryLatencyMillis) {
@@ -386,6 +397,7 @@
}
/** Sets time used to restore the index. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setIndexRestorationLatencyMillis(
int indexRestorationLatencyMillis) {
@@ -394,6 +406,7 @@
}
/** Sets time used to recover the schema store. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setSchemaStoreRecoveryLatencyMillis(
int schemaStoreRecoveryLatencyMillis) {
@@ -405,6 +418,7 @@
* Sets Native Document Store Data status.
* status is defined in external/icing/proto/icing/proto/logging.proto
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setDocumentStoreDataStatus(
@DocumentStoreDataStatus int documentStoreDataStatus) {
@@ -416,6 +430,7 @@
* Sets number of documents currently in document store. Those may include alive, deleted,
* and expired documents.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setDocumentCount(int numDocuments) {
mNativeNumDocuments = numDocuments;
@@ -423,6 +438,7 @@
}
/** Sets number of schema types currently in the schema store. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setSchemaTypeCount(int numSchemaTypes) {
mNativeNumSchemaTypes = numSchemaTypes;
@@ -430,6 +446,7 @@
}
/** Sets whether we had to reset the index, losing all data, as part of initialization. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setHasReset(boolean hasReset) {
mHasReset = hasReset;
@@ -437,6 +454,7 @@
}
/** Sets the status of the reset, if one was performed according to {@link #setHasReset}. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setResetStatusCode(@AppSearchResult.ResultCode int resetStatusCode) {
mResetStatusCode = resetStatusCode;
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/OptimizeStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/OptimizeStats.java
index b7dcae0..2a47183 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/OptimizeStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/OptimizeStats.java
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.app.AppSearchResult;
import androidx.core.util.Preconditions;
@@ -156,6 +157,7 @@
long mNativeTimeSinceLastOptimizeMillis;
/** Sets the status code. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
mStatusCode = statusCode;
@@ -163,6 +165,7 @@
}
/** Sets total latency in millis. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setTotalLatencyMillis(int totalLatencyMillis) {
mTotalLatencyMillis = totalLatencyMillis;
@@ -170,6 +173,7 @@
}
/** Sets native latency in millis. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setNativeLatencyMillis(int nativeLatencyMillis) {
mNativeLatencyMillis = nativeLatencyMillis;
@@ -177,6 +181,7 @@
}
/** Sets time used to optimize the document store. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setDocumentStoreOptimizeLatencyMillis(
int documentStoreOptimizeLatencyMillis) {
@@ -185,6 +190,7 @@
}
/** Sets time used to restore the index. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setIndexRestorationLatencyMillis(int indexRestorationLatencyMillis) {
mNativeIndexRestorationLatencyMillis = indexRestorationLatencyMillis;
@@ -192,6 +198,7 @@
}
/** Sets number of documents before the optimization. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setOriginalDocumentCount(int originalDocumentCount) {
mNativeOriginalDocumentCount = originalDocumentCount;
@@ -199,6 +206,7 @@
}
/** Sets number of documents deleted during the optimization. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setDeletedDocumentCount(int deletedDocumentCount) {
mNativeDeletedDocumentCount = deletedDocumentCount;
@@ -206,6 +214,7 @@
}
/** Sets number of documents expired during the optimization. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setExpiredDocumentCount(int expiredDocumentCount) {
mNativeExpiredDocumentCount = expiredDocumentCount;
@@ -213,6 +222,7 @@
}
/** Sets Storage size in bytes before optimization. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setStorageSizeBeforeBytes(long storageSizeBeforeBytes) {
mNativeStorageSizeBeforeBytes = storageSizeBeforeBytes;
@@ -220,6 +230,7 @@
}
/** Sets storage size in bytes after optimization. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setStorageSizeAfterBytes(long storageSizeAfterBytes) {
mNativeStorageSizeAfterBytes = storageSizeAfterBytes;
@@ -229,6 +240,7 @@
/**
* Sets the amount the time since the last optimize ran calculated using wall clock time.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setTimeSinceLastOptimizeMillis(long timeSinceLastOptimizeMillis) {
mNativeTimeSinceLastOptimizeMillis = timeSinceLastOptimizeMillis;
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java
index e9a25fd..3378df7 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.app.AppSearchResult;
import androidx.core.util.Preconditions;
@@ -169,6 +170,7 @@
}
/** Sets the status code. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
mStatusCode = statusCode;
@@ -176,6 +178,7 @@
}
/** Sets total latency in millis. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setTotalLatencyMillis(int totalLatencyMillis) {
mTotalLatencyMillis = totalLatencyMillis;
@@ -183,6 +186,7 @@
}
/** Sets how much time we spend for generating document proto, in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setGenerateDocumentProtoLatencyMillis(
int generateDocumentProtoLatencyMillis) {
@@ -194,6 +198,7 @@
* Sets how much time we spend for rewriting types and namespaces in document, in
* milliseconds.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setRewriteDocumentTypesLatencyMillis(int rewriteDocumentTypesLatencyMillis) {
mRewriteDocumentTypesLatencyMillis = rewriteDocumentTypesLatencyMillis;
@@ -201,6 +206,7 @@
}
/** Sets the native latency, in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setNativeLatencyMillis(int nativeLatencyMillis) {
mNativeLatencyMillis = nativeLatencyMillis;
@@ -208,6 +214,7 @@
}
/** Sets how much time we spend on document store, in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setNativeDocumentStoreLatencyMillis(int nativeDocumentStoreLatencyMillis) {
mNativeDocumentStoreLatencyMillis = nativeDocumentStoreLatencyMillis;
@@ -215,6 +222,7 @@
}
/** Sets the native index latency, in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setNativeIndexLatencyMillis(int nativeIndexLatencyMillis) {
mNativeIndexLatencyMillis = nativeIndexLatencyMillis;
@@ -222,6 +230,7 @@
}
/** Sets how much time we spend on merging indices, in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setNativeIndexMergeLatencyMillis(int nativeIndexMergeLatencyMillis) {
mNativeIndexMergeLatencyMillis = nativeIndexMergeLatencyMillis;
@@ -229,6 +238,7 @@
}
/** Sets document size, in bytes. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setNativeDocumentSizeBytes(int nativeDocumentSizeBytes) {
mNativeDocumentSizeBytes = nativeDocumentSizeBytes;
@@ -236,6 +246,7 @@
}
/** Sets number of tokens indexed in native. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setNativeNumTokensIndexed(int nativeNumTokensIndexed) {
mNativeNumTokensIndexed = nativeNumTokensIndexed;
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 7eb4820..ffb3f3a 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
@@ -19,6 +19,7 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.app.AppSearchResult;
import androidx.appsearch.app.RemoveByDocumentIdRequest;
import androidx.appsearch.app.SearchSpec;
@@ -148,6 +149,7 @@
}
/** Sets the status code. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
mStatusCode = statusCode;
@@ -155,6 +157,7 @@
}
/** Sets total latency in millis. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setTotalLatencyMillis(int totalLatencyMillis) {
mTotalLatencyMillis = totalLatencyMillis;
@@ -162,6 +165,7 @@
}
/** Sets native latency in millis. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setNativeLatencyMillis(int nativeLatencyMillis) {
mNativeLatencyMillis = nativeLatencyMillis;
@@ -169,6 +173,7 @@
}
/** Sets delete type for this call. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setDeleteType(@DeleteType int nativeDeleteType) {
mNativeDeleteType = nativeDeleteType;
@@ -176,6 +181,7 @@
}
/** Sets how many documents get deleted for this call. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setDeletedDocumentCount(int nativeNumDocumentsDeleted) {
mNativeNumDocumentsDeleted = nativeNumDocumentsDeleted;
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 bc46326..6945221 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
@@ -19,7 +19,9 @@
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.StringPropertyConfig.JoinableValueType;
import androidx.appsearch.app.SearchSpec;
import androidx.core.util.Preconditions;
@@ -129,7 +131,12 @@
private final int mJavaToNativeJniLatencyMillis;
/** Time used to send data across the JNI boundary from native to java side. */
private final int mNativeToJavaJniLatencyMillis;
-
+ /** The type of join performed. Zero if no join is performed */
+ @JoinableValueType private final int mJoinType;
+ /** The total number of joined documents in the current page. */
+ private final int mNativeNumJoinedResultsCurrentPage;
+ /** Time taken to join documents together. */
+ private final int mNativeJoinLatencyMillis;
SearchStats(@NonNull Builder builder) {
Preconditions.checkNotNull(builder);
@@ -160,6 +167,9 @@
mNativeLockAcquisitionLatencyMillis = builder.mNativeLockAcquisitionLatencyMillis;
mJavaToNativeJniLatencyMillis = builder.mJavaToNativeJniLatencyMillis;
mNativeToJavaJniLatencyMillis = builder.mNativeToJavaJniLatencyMillis;
+ mJoinType = builder.mJoinType;
+ mNativeNumJoinedResultsCurrentPage = builder.mNativeNumJoinedResultsCurrentPage;
+ mNativeJoinLatencyMillis = builder.mNativeJoinLatencyMillis;
}
/** Returns the package name of the session. */
@@ -317,6 +327,21 @@
return mNativeToJavaJniLatencyMillis;
}
+ /** Returns the type of join performed. Blank if no join is performed */
+ public @JoinableValueType int getJoinType() {
+ return mJoinType;
+ }
+
+ /** Returns the total number of joined documents in the current page. */
+ public int getNumJoinedResultsCurrentPage() {
+ return mNativeNumJoinedResultsCurrentPage;
+ }
+
+ /** Returns the time taken to join documents together. */
+ public int getJoinLatencyMillis() {
+ return mNativeJoinLatencyMillis;
+ }
+
/** Builder for {@link SearchStats} */
public static class Builder {
@NonNull
@@ -349,7 +374,9 @@
int mNativeLockAcquisitionLatencyMillis;
int mJavaToNativeJniLatencyMillis;
int mNativeToJavaJniLatencyMillis;
-
+ @JoinableValueType int mJoinType;
+ int mNativeNumJoinedResultsCurrentPage;
+ int mNativeJoinLatencyMillis;
/**
* Constructor
@@ -363,13 +390,15 @@
}
/** Sets the database used by the session. */
+ @CanIgnoreReturnValue
@NonNull
- public Builder setDatabase(@NonNull String database) {
- mDatabase = Preconditions.checkNotNull(database);
+ public Builder setDatabase(@Nullable String database) {
+ mDatabase = database;
return this;
}
/** Sets the status of the search. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
mStatusCode = statusCode;
@@ -377,6 +406,7 @@
}
/** Sets total latency for the search. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setTotalLatencyMillis(int totalLatencyMillis) {
mTotalLatencyMillis = totalLatencyMillis;
@@ -384,6 +414,7 @@
}
/** Sets time used to rewrite the search spec. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setRewriteSearchSpecLatencyMillis(int rewriteSearchSpecLatencyMillis) {
mRewriteSearchSpecLatencyMillis = rewriteSearchSpecLatencyMillis;
@@ -391,6 +422,7 @@
}
/** Sets time used to rewrite the search results. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setRewriteSearchResultLatencyMillis(int rewriteSearchResultLatencyMillis) {
mRewriteSearchResultLatencyMillis = rewriteSearchResultLatencyMillis;
@@ -398,6 +430,7 @@
}
/** Sets time passed while waiting to acquire the lock during Java function calls. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setJavaLockAcquisitionLatencyMillis(int javaLockAcquisitionLatencyMillis) {
mJavaLockAcquisitionLatencyMillis = javaLockAcquisitionLatencyMillis;
@@ -408,6 +441,7 @@
* Sets time spent on ACL checking, which is the time spent filtering namespaces based on
* package permissions and Android permission access.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setAclCheckLatencyMillis(int aclCheckLatencyMillis) {
mAclCheckLatencyMillis = aclCheckLatencyMillis;
@@ -415,6 +449,7 @@
}
/** Sets overall time used for the native function calls. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setNativeLatencyMillis(int nativeLatencyMillis) {
mNativeLatencyMillis = nativeLatencyMillis;
@@ -422,6 +457,7 @@
}
/** Sets number of terms in the search string. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setTermCount(int termCount) {
mNativeNumTerms = termCount;
@@ -429,6 +465,7 @@
}
/** Sets length of the search string. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setQueryLength(int queryLength) {
mNativeQueryLength = queryLength;
@@ -436,6 +473,7 @@
}
/** Sets number of namespaces filtered. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setFilteredNamespaceCount(int filteredNamespaceCount) {
mNativeNumNamespacesFiltered = filteredNamespaceCount;
@@ -443,6 +481,7 @@
}
/** Sets number of schema types filtered. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setFilteredSchemaTypeCount(int filteredSchemaTypeCount) {
mNativeNumSchemaTypesFiltered = filteredSchemaTypeCount;
@@ -450,6 +489,7 @@
}
/** Sets the requested number of results in one page. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setRequestedPageSize(int requestedPageSize) {
mNativeRequestedPageSize = requestedPageSize;
@@ -457,6 +497,7 @@
}
/** Sets the actual number of results returned in the current page. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setCurrentPageReturnedResultCount(
int currentPageReturnedResultCount) {
@@ -469,6 +510,7 @@
* not, Icing will fetch the results from cache so that some steps
* may be skipped.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setIsFirstPage(boolean nativeIsFirstPage) {
mNativeIsFirstPage = nativeIsFirstPage;
@@ -479,6 +521,7 @@
* Sets time used to parse the query, including 2 parts: tokenizing and
* transforming tokens into an iterator tree.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setParseQueryLatencyMillis(int parseQueryLatencyMillis) {
mNativeParseQueryLatencyMillis = parseQueryLatencyMillis;
@@ -486,6 +529,7 @@
}
/** Sets strategy of scoring and ranking. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setRankingStrategy(
@SearchSpec.RankingStrategy int rankingStrategy) {
@@ -494,6 +538,7 @@
}
/** Sets number of documents scored. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setScoredDocumentCount(int scoredDocumentCount) {
mNativeNumDocumentsScored = scoredDocumentCount;
@@ -501,6 +546,7 @@
}
/** Sets time used to score the raw results. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setScoringLatencyMillis(int scoringLatencyMillis) {
mNativeScoringLatencyMillis = scoringLatencyMillis;
@@ -508,6 +554,7 @@
}
/** Sets time used to rank the scored results. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setRankingLatencyMillis(int rankingLatencyMillis) {
mNativeRankingLatencyMillis = rankingLatencyMillis;
@@ -515,6 +562,7 @@
}
/** Sets time used to fetch the document protos. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setDocumentRetrievingLatencyMillis(
int documentRetrievingLatencyMillis) {
@@ -523,6 +571,7 @@
}
/** Sets how many snippets are calculated. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setResultWithSnippetsCount(int resultWithSnippetsCount) {
mNativeNumResultsWithSnippets = resultWithSnippetsCount;
@@ -530,6 +579,7 @@
}
/** Sets time passed while waiting to acquire the lock during native function calls. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setNativeLockAcquisitionLatencyMillis(
int nativeLockAcquisitionLatencyMillis) {
@@ -538,6 +588,7 @@
}
/** Sets time used to send data across the JNI boundary from java to native side. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setJavaToNativeJniLatencyMillis(int javaToNativeJniLatencyMillis) {
mJavaToNativeJniLatencyMillis = javaToNativeJniLatencyMillis;
@@ -545,12 +596,34 @@
}
/** Sets time used to send data across the JNI boundary from native to java side. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setNativeToJavaJniLatencyMillis(int nativeToJavaJniLatencyMillis) {
mNativeToJavaJniLatencyMillis = nativeToJavaJniLatencyMillis;
return this;
}
+ /** Sets whether or not this is a join query */
+ @NonNull
+ public Builder setJoinType(@JoinableValueType int joinType) {
+ mJoinType = joinType;
+ return this;
+ }
+
+ /** Set the total number of joined documents in a page. */
+ @NonNull
+ public Builder setNativeNumJoinedResultsCurrentPage(int nativeNumJoinedResultsCurrentPage) {
+ mNativeNumJoinedResultsCurrentPage = nativeNumJoinedResultsCurrentPage;
+ return this;
+ }
+
+ /** Sets time it takes to join documents together in icing. */
+ @NonNull
+ public Builder setNativeJoinLatencyMillis(int nativeJoinLatencyMillis) {
+ mNativeJoinLatencyMillis = nativeJoinLatencyMillis;
+ 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/stats/SetSchemaStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SetSchemaStats.java
index c052eb8..6ff7d8e 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SetSchemaStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SetSchemaStats.java
@@ -21,6 +21,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.app.AppSearchResult;
import androidx.appsearch.stats.SchemaMigrationStats;
import androidx.core.util.Preconditions;
@@ -224,7 +225,8 @@
}
/** Gets the type indicate how this set schema call relative to schema migration cases */
- public @SchemaMigrationStats.SchemaMigrationCallType int getSchemaMigrationCallType() {
+ @SchemaMigrationStats.SchemaMigrationCallType
+ public int getSchemaMigrationCallType() {
return mSchemaMigrationCallType;
}
@@ -266,6 +268,7 @@
}
/** Sets the status of the SetSchema action. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
mStatusCode = statusCode;
@@ -273,6 +276,7 @@
}
/** Sets total latency for the SetSchema action in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setTotalLatencyMillis(int totalLatencyMillis) {
mTotalLatencyMillis = totalLatencyMillis;
@@ -280,6 +284,7 @@
}
/** Sets number of new types. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setNewTypeCount(int newTypeCount) {
mNewTypeCount = newTypeCount;
@@ -287,6 +292,7 @@
}
/** Sets number of deleted types. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setDeletedTypeCount(int deletedTypeCount) {
mDeletedTypeCount = deletedTypeCount;
@@ -294,6 +300,7 @@
}
/** Sets number of compatible type changes. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setCompatibleTypeChangeCount(int compatibleTypeChangeCount) {
mCompatibleTypeChangeCount = compatibleTypeChangeCount;
@@ -301,6 +308,7 @@
}
/** Sets number of index-incompatible type changes. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setIndexIncompatibleTypeChangeCount(int indexIncompatibleTypeChangeCount) {
mIndexIncompatibleTypeChangeCount = indexIncompatibleTypeChangeCount;
@@ -308,6 +316,7 @@
}
/** Sets number of backwards-incompatible type changes. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setBackwardsIncompatibleTypeChangeCount(
int backwardsIncompatibleTypeChangeCount) {
@@ -316,6 +325,7 @@
}
/** Sets total latency for the SetSchema in native action in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setVerifyIncomingCallLatencyMillis(int verifyIncomingCallLatencyMillis) {
mVerifyIncomingCallLatencyMillis = verifyIncomingCallLatencyMillis;
@@ -323,6 +333,7 @@
}
/** Sets total latency for the SetSchema in native action in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setExecutorAcquisitionLatencyMillis(int executorAcquisitionLatencyMillis) {
mExecutorAcquisitionLatencyMillis = executorAcquisitionLatencyMillis;
@@ -330,6 +341,7 @@
}
/** Sets latency for the rebuild schema object from bundle action in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setRebuildFromBundleLatencyMillis(int rebuildFromBundleLatencyMillis) {
mRebuildFromBundleLatencyMillis = rebuildFromBundleLatencyMillis;
@@ -339,6 +351,7 @@
/**
* Sets latency for waiting to acquire the lock during Java function calls in milliseconds.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setJavaLockAcquisitionLatencyMillis(int javaLockAcquisitionLatencyMillis) {
mJavaLockAcquisitionLatencyMillis = javaLockAcquisitionLatencyMillis;
@@ -346,6 +359,7 @@
}
/** Sets latency for the rewrite the schema proto action in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setRewriteSchemaLatencyMillis(int rewriteSchemaLatencyMillis) {
mRewriteSchemaLatencyMillis = rewriteSchemaLatencyMillis;
@@ -353,6 +367,7 @@
}
/** Sets total latency for a single set schema in native action in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setTotalNativeLatencyMillis(int totalNativeLatencyMillis) {
mTotalNativeLatencyMillis = totalNativeLatencyMillis;
@@ -360,6 +375,7 @@
}
/** Sets latency for the apply visibility settings action in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setVisibilitySettingLatencyMillis(int visibilitySettingLatencyMillis) {
mVisibilitySettingLatencyMillis = visibilitySettingLatencyMillis;
@@ -367,6 +383,7 @@
}
/** Sets latency for converting to SetSchemaResponseInternal object in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setConvertToResponseLatencyMillis(int convertToResponseLatencyMillis) {
mConvertToResponseLatencyMillis = convertToResponseLatencyMillis;
@@ -374,6 +391,7 @@
}
/** Sets latency for the dispatch change notification action in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setDispatchChangeNotificationsLatencyMillis(
int dispatchChangeNotificationsLatencyMillis) {
@@ -382,6 +400,7 @@
}
/** Sets latency for the optimization action in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setOptimizeLatencyMillis(int optimizeLatencyMillis) {
mOptimizeLatencyMillis = optimizeLatencyMillis;
@@ -389,6 +408,7 @@
}
/** Sets whether this package is observed and we should prepare change notifications. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setIsPackageObserved(boolean isPackageObserved) {
mIsPackageObserved = isPackageObserved;
@@ -396,6 +416,7 @@
}
/** Sets latency for the old schema action in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setGetOldSchemaLatencyMillis(int getOldSchemaLatencyMillis) {
mGetOldSchemaLatencyMillis = getOldSchemaLatencyMillis;
@@ -403,6 +424,7 @@
}
/** Sets latency for the registered observer action in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setGetObserverLatencyMillis(int getObserverLatencyMillis) {
mGetObserverLatencyMillis = getObserverLatencyMillis;
@@ -410,6 +432,7 @@
}
/** Sets latency for the preparing change notification action in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setPreparingChangeNotificationLatencyMillis(
int preparingChangeNotificationLatencyMillis) {
@@ -418,6 +441,7 @@
}
/** Sets the type indicate how this set schema call relative to schema migration cases */
+ @CanIgnoreReturnValue
@NonNull
public Builder setSchemaMigrationCallType(
@SchemaMigrationStats.SchemaMigrationCallType int schemaMigrationCallType) {
diff --git a/appsearch/appsearch-platform-storage/lint-baseline.xml b/appsearch/appsearch-platform-storage/lint-baseline.xml
index d82c712..c09fd6a 100644
--- a/appsearch/appsearch-platform-storage/lint-baseline.xml
+++ b/appsearch/appsearch-platform-storage/lint-baseline.xml
@@ -3,7 +3,7 @@
<issue
id="WrongConstant"
- message="Must be one of: AppSearchResult.RESULT_OK, AppSearchResult.RESULT_UNKNOWN_ERROR, AppSearchResult.RESULT_INTERNAL_ERROR, AppSearchResult.RESULT_INVALID_ARGUMENT, AppSearchResult.RESULT_IO_ERROR, AppSearchResult.RESULT_OUT_OF_SPACE, AppSearchResult.RESULT_NOT_FOUND, AppSearchResult.RESULT_INVALID_SCHEMA, AppSearchResult.RESULT_SECURITY_ERROR"
+ message="Must be one of: AppSearchResult.RESULT_OK, AppSearchResult.RESULT_UNKNOWN_ERROR, AppSearchResult.RESULT_INTERNAL_ERROR, AppSearchResult.RESULT_INVALID_ARGUMENT, AppSearchResult.RESULT_IO_ERROR, AppSearchResult.RESULT_OUT_OF_SPACE, AppSearchResult.RESULT_NOT_FOUND, AppSearchResult.RESULT_INVALID_SCHEMA, AppSearchResult.RESULT_SECURITY_ERROR, AppSearchResult.RESULT_DENIED, but could be AppSearchResult.RESULT_OK, AppSearchResult.RESULT_UNKNOWN_ERROR, AppSearchResult.RESULT_INTERNAL_ERROR, AppSearchResult.RESULT_INVALID_ARGUMENT, AppSearchResult.RESULT_IO_ERROR, AppSearchResult.RESULT_OUT_OF_SPACE, AppSearchResult.RESULT_NOT_FOUND, AppSearchResult.RESULT_INVALID_SCHEMA, AppSearchResult.RESULT_SECURITY_ERROR"
errorLine1=" platformResult.getResultCode(), platformResult.getErrorMessage()));"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -12,7 +12,7 @@
<issue
id="WrongConstant"
- message="Must be one of: AppSearchResult.RESULT_OK, AppSearchResult.RESULT_UNKNOWN_ERROR, AppSearchResult.RESULT_INTERNAL_ERROR, AppSearchResult.RESULT_INVALID_ARGUMENT, AppSearchResult.RESULT_IO_ERROR, AppSearchResult.RESULT_OUT_OF_SPACE, AppSearchResult.RESULT_NOT_FOUND, AppSearchResult.RESULT_INVALID_SCHEMA, AppSearchResult.RESULT_SECURITY_ERROR"
+ message="Must be one of: AppSearchResult.RESULT_OK, AppSearchResult.RESULT_UNKNOWN_ERROR, AppSearchResult.RESULT_INTERNAL_ERROR, AppSearchResult.RESULT_INVALID_ARGUMENT, AppSearchResult.RESULT_IO_ERROR, AppSearchResult.RESULT_OUT_OF_SPACE, AppSearchResult.RESULT_NOT_FOUND, AppSearchResult.RESULT_INVALID_SCHEMA, AppSearchResult.RESULT_SECURITY_ERROR, AppSearchResult.RESULT_DENIED, but could be AppSearchResult.RESULT_OK, AppSearchResult.RESULT_UNKNOWN_ERROR, AppSearchResult.RESULT_INTERNAL_ERROR, AppSearchResult.RESULT_INVALID_ARGUMENT, AppSearchResult.RESULT_IO_ERROR, AppSearchResult.RESULT_OUT_OF_SPACE, AppSearchResult.RESULT_NOT_FOUND, AppSearchResult.RESULT_INVALID_SCHEMA, AppSearchResult.RESULT_SECURITY_ERROR"
errorLine1=" namespaceResult.getResultCode(),"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -148,6 +148,24 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
+ errorLine1="public class SearchSuggestionResultToPlatformConverter {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionResultToPlatformConverter.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1="public final class SearchSuggestionSpecToPlatformConverter {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionSpecToPlatformConverter.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
errorLine1="public final class SetSchemaRequestToPlatformConverter {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -155,99 +173,9 @@
</issue>
<issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 33; however, the containing class androidx.appsearch.platformstorage.converter.GetSchemaResponseToPlatformConverter is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" platformResponse.getSchemaTypesNotDisplayedBySystem()) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/appsearch/platformstorage/converter/GetSchemaResponseToPlatformConverter.java"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 33; however, the containing class androidx.appsearch.platformstorage.converter.GetSchemaResponseToPlatformConverter is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" platformResponse.getRequiredPermissionsForSchemaTypeVisibility().entrySet()) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/appsearch/platformstorage/converter/GetSchemaResponseToPlatformConverter.java"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 33; however, the containing class androidx.appsearch.platformstorage.converter.GetSchemaResponseToPlatformConverter is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" platformResponse.getSchemaTypesVisibleToPackages();"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/appsearch/platformstorage/converter/GetSchemaResponseToPlatformConverter.java"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 33; however, the containing class androidx.appsearch.platformstorage.GlobalSearchSessionImpl is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" mPlatformSession.getByDocumentId(packageName, databaseName,"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 33; however, the containing class androidx.appsearch.platformstorage.GlobalSearchSessionImpl is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" mPlatformSession.getSchema("
- errorLine2=" ~~~~~~~~~">
- <location
- file="src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 33; however, the containing class androidx.appsearch.platformstorage.GlobalSearchSessionImpl is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" mPlatformSession.registerObserverCallback("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 33; however, the containing class androidx.appsearch.platformstorage.GlobalSearchSessionImpl is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" mPlatformSession.unregisterObserverCallback(targetPackageName, frameworkCallback);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 33; however, the containing class androidx.appsearch.platformstorage.converter.SearchResultToPlatformConverter is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" platformMatchInfo.getSubmatchRange().getStart(),"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 33; however, the containing class androidx.appsearch.platformstorage.converter.SearchResultToPlatformConverter is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" platformMatchInfo.getSubmatchRange().getEnd()));"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 33; however, the containing class androidx.appsearch.platformstorage.converter.SetSchemaRequestToPlatformConverter is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" platformBuilder.addRequiredPermissionsForSchemaTypeVisibility("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java"/>
- </issue>
-
- <issue
id="PrereleaseSdkCoreDependency"
- message="Prelease SDK check isAtLeastT cannot be called as this project has a versioned dependency on androidx.core:core"
- errorLine1=" return BuildCompat.isAtLeastT();"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" return BuildCompat.isAtLeastU();"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java"/>
@@ -255,20 +183,92 @@
<issue
id="PrereleaseSdkCoreDependency"
- message="Prelease SDK check isAtLeastT cannot be called as this project has a versioned dependency on androidx.core:core"
- errorLine1=" if (BuildCompat.isAtLeastT()) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (!BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
- file="src/main/java/androidx/appsearch/platformstorage/converter/GetSchemaResponseToPlatformConverter.java"/>
+ file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"/>
</issue>
<issue
id="PrereleaseSdkCoreDependency"
- message="Prelease SDK check isAtLeastT cannot be called as this project has a versioned dependency on androidx.core:core"
- errorLine1=" if (BuildCompat.isAtLeastT()) {"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (!BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java"/>
</issue>
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (!BuildCompat.isAtLeastU() && mSearchSpec.getJoinSpec() != null) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (!BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (!BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (!BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (!BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java"/>
+ </issue>
+
</issues>
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 2f9336e..e52dd19 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
@@ -15,6 +15,9 @@
*/
package androidx.appsearch.platformstorage;
+import android.annotation.SuppressLint;
+import android.os.Build;
+
import androidx.annotation.NonNull;
import androidx.appsearch.app.Features;
import androidx.core.os.BuildCompat;
@@ -26,7 +29,8 @@
final class FeaturesImpl implements Features {
@Override
- // TODO(b/201316758): Remove once BuildCompat.isAtLeastT is removed
+ // TODO(b/265311462): Remove these two lines once BuildCompat.isAtLeastU() is removed
+ @SuppressLint("NewApi")
@BuildCompat.PrereleaseSdkCheck
public boolean isFeatureSupported(@NonNull String feature) {
switch (feature) {
@@ -40,37 +44,33 @@
case Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK:
// fall through
case Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH:
- // fall through
- return BuildCompat.isAtLeastT();
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU;
// 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.
- // 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.
- // fall through
- case SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION:
- // TODO(b/261474063) : Update to reflect support in Android U+ once advanced
- // ranking becomes available.
- // 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.
- // 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
+ // fall through
+ case Features.NUMERIC_SEARCH:
+ // fall through
+ case Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION:
+ // fall through
+ case Features.SEARCH_SPEC_PROPERTY_WEIGHTS:
+ // fall through
+ case Features.SEARCH_SUGGESTION:
+ // fall through
+ case Features.TOKENIZER_TYPE_RFC822:
+ // fall through
+ case Features.VERBATIM_SEARCH:
+ return BuildCompat.isAtLeastU();
+
+ // Beyond Android U features
+ 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.SCHEMA_SET_DELETION_PROPAGATION:
+ // TODO(b/268521214) : Update when feature is ready in service-appsearch.
return false;
default:
return false;
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
index 1ee50ec..dbebde8 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
@@ -16,8 +16,11 @@
package androidx.appsearch.platformstorage;
import android.annotation.SuppressLint;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.BatchResultCallback;
import android.os.Build;
+import androidx.annotation.DoNotInline;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
@@ -52,9 +55,10 @@
import java.util.Map;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
- * An implementation of {@link androidx.appsearch.app.GlobalSearchSession} which proxies to a
+ * An implementation of {@link GlobalSearchSession} which proxies to a
* platform {@link android.app.appsearch.GlobalSearchSession}.
*
* @hide
@@ -80,7 +84,6 @@
mFeatures = Preconditions.checkNotNull(features);
}
- @BuildCompat.PrereleaseSdkCheck
@NonNull
@Override
public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
@@ -95,14 +98,16 @@
Preconditions.checkNotNull(request);
ResolvableFuture<AppSearchBatchResult<String, GenericDocument>> future =
ResolvableFuture.create();
- mPlatformSession.getByDocumentId(packageName, databaseName,
- RequestToPlatformConverter.toPlatformGetByDocumentIdRequest(request),
- mExecutor,
+ ApiHelperForT.getByDocumentId(mPlatformSession, packageName, databaseName,
+ RequestToPlatformConverter.toPlatformGetByDocumentIdRequest(request), mExecutor,
new BatchResultCallbackAdapter<>(
future, GenericDocumentToPlatformConverter::toJetpackGenericDocument));
return future;
}
+ // TODO(b/265311462): Remove these two lines once BuildCompat.isAtLeastU() is removed
+ @SuppressLint("NewApi")
+ @BuildCompat.PrereleaseSdkCheck
@Override
@NonNull
public SearchResults search(
@@ -131,6 +136,8 @@
return future;
}
+ // TODO(b/265311462): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastU() is removed.
@BuildCompat.PrereleaseSdkCheck
@NonNull
@Override
@@ -144,10 +151,7 @@
+ " is not supported on this AppSearch implementation.");
}
ResolvableFuture<GetSchemaResponse> future = ResolvableFuture.create();
- mPlatformSession.getSchema(
- packageName,
- databaseName,
- mExecutor,
+ ApiHelperForT.getSchema(mPlatformSession, packageName, databaseName, mExecutor,
result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
result,
future,
@@ -161,9 +165,7 @@
return mFeatures;
}
- // TODO(b/193494000): Remove these two lines once BuildCompat.isAtLeastT() is removed.
- @SuppressLint("NewApi")
- @BuildCompat.PrereleaseSdkCheck
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Override
public void registerObserverCallback(
@NonNull String targetPackageName,
@@ -213,10 +215,8 @@
// Regardless of whether this stub was fresh or not, we have to register it again
// because the user might be supplying a different spec.
try {
- mPlatformSession.registerObserverCallback(
- targetPackageName,
- ObserverSpecToPlatformConverter.toPlatformObserverSpec(spec),
- executor,
+ ApiHelperForT.registerObserverCallback(mPlatformSession, targetPackageName,
+ ObserverSpecToPlatformConverter.toPlatformObserverSpec(spec), executor,
frameworkCallback);
} catch (android.app.appsearch.exceptions.AppSearchException e) {
throw new AppSearchException((int) e.getResultCode(), e.getMessage(), e.getCause());
@@ -229,8 +229,6 @@
}
}
- @SuppressLint("NewApi")
- @BuildCompat.PrereleaseSdkCheck
@Override
public void unregisterObserverCallback(
@NonNull String targetPackageName, @NonNull ObserverCallback observer)
@@ -253,7 +251,8 @@
}
try {
- mPlatformSession.unregisterObserverCallback(targetPackageName, frameworkCallback);
+ ApiHelperForT.unregisterObserverCallback(mPlatformSession, targetPackageName,
+ frameworkCallback);
} catch (android.app.appsearch.exceptions.AppSearchException e) {
throw new AppSearchException((int) e.getResultCode(), e.getMessage(), e.getCause());
}
@@ -267,4 +266,43 @@
public void close() {
mPlatformSession.close();
}
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private static class ApiHelperForT {
+ private ApiHelperForT() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static void getByDocumentId(android.app.appsearch.GlobalSearchSession platformSession,
+ String packageName, String databaseName,
+ android.app.appsearch.GetByDocumentIdRequest request, Executor executor,
+ BatchResultCallback<String, android.app.appsearch.GenericDocument> callback) {
+ platformSession.getByDocumentId(packageName, databaseName, request, executor, callback);
+ }
+
+ @DoNotInline
+ static void getSchema(android.app.appsearch.GlobalSearchSession platformSessions,
+ String packageName, String databaseName, Executor executor,
+ Consumer<AppSearchResult<android.app.appsearch.GetSchemaResponse>> callback) {
+ platformSessions.getSchema(packageName, databaseName, executor, callback);
+ }
+
+ @DoNotInline
+ static void registerObserverCallback(
+ android.app.appsearch.GlobalSearchSession platformSession, String targetPackageName,
+ android.app.appsearch.observer.ObserverSpec spec, Executor executor,
+ android.app.appsearch.observer.ObserverCallback observer)
+ throws android.app.appsearch.exceptions.AppSearchException {
+ platformSession.registerObserverCallback(targetPackageName, spec, executor, observer);
+ }
+
+ @DoNotInline
+ static void unregisterObserverCallback(
+ android.app.appsearch.GlobalSearchSession platformSession, String targetPackageName,
+ android.app.appsearch.observer.ObserverCallback observer)
+ throws android.app.appsearch.exceptions.AppSearchException {
+ platformSession.unregisterObserverCallback(targetPackageName, observer);
+ }
+ }
}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java
index 6fde6d6..1217bf6 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java
@@ -58,13 +58,14 @@
mExecutor = Preconditions.checkNotNull(executor);
}
+ // TODO(b/265311462): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastU() is removed.
@SuppressLint("WrongConstant")
@Override
@NonNull
@BuildCompat.PrereleaseSdkCheck
public ListenableFuture<List<SearchResult>> getNextPageAsync() {
- // TODO(b/256022027): add isAtLeastU check after Android U.
- if (mSearchSpec.getJoinSpec() != null) {
+ if (!BuildCompat.isAtLeastU() && mSearchSpec.getJoinSpec() != null) {
throw new UnsupportedOperationException("Searching with a SearchSpec containing a "
+ "JoinSpec is not supported on this AppSearch implementation.");
}
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 4e4f3db..f104e03 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 android.annotation.SuppressLint;
+import android.app.appsearch.AppSearchResult;
import android.os.Build;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
@@ -43,6 +46,8 @@
import androidx.appsearch.platformstorage.converter.RequestToPlatformConverter;
import androidx.appsearch.platformstorage.converter.ResponseToPlatformConverter;
import androidx.appsearch.platformstorage.converter.SearchSpecToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.SearchSuggestionResultToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.SearchSuggestionSpecToPlatformConverter;
import androidx.appsearch.platformstorage.converter.SetSchemaRequestToPlatformConverter;
import androidx.appsearch.platformstorage.util.BatchResultCallbackAdapter;
import androidx.concurrent.futures.ResolvableFuture;
@@ -54,6 +59,7 @@
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* An implementation of {@link AppSearchSession} which proxies to a platform
@@ -76,9 +82,11 @@
mFeatures = Preconditions.checkNotNull(features);
}
+ // TODO(b/265311462): Remove these two lines once BuildCompat.isAtLeastU() is removed
+ @SuppressLint("NewApi")
+ @BuildCompat.PrereleaseSdkCheck
@Override
@NonNull
- @BuildCompat.PrereleaseSdkCheck
public ListenableFuture<SetSchemaResponse> setSchemaAsync(@NonNull SetSchemaRequest request) {
Preconditions.checkNotNull(request);
ResolvableFuture<SetSchemaResponse> future = ResolvableFuture.create();
@@ -93,9 +101,11 @@
return future;
}
+ // TODO(b/265311462): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastU() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@Override
@NonNull
- @BuildCompat.PrereleaseSdkCheck
public ListenableFuture<GetSchemaResponse> getSchemaAsync() {
ResolvableFuture<GetSchemaResponse> future = ResolvableFuture.create();
mPlatformSession.getSchema(
@@ -146,6 +156,9 @@
return future;
}
+ // TODO(b/265311462): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastU() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@Override
@NonNull
public SearchResults search(
@@ -160,13 +173,34 @@
return new SearchResultsImpl(platformSearchResults, searchSpec, mExecutor);
}
+ // TODO(b/265311462): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastU() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
@Override
public ListenableFuture<List<SearchSuggestionResult>> searchSuggestionAsync(
- @NonNull String suggestionQueryExpression, @NonNull SearchSuggestionSpec searchSpec) {
- // TODO(b/227356108) Implement this after we export to framework.
- throw new UnsupportedOperationException(
- "Search Suggestion is not supported on this AppSearch implementation.");
+ @NonNull String suggestionQueryExpression,
+ @NonNull SearchSuggestionSpec searchSuggestionSpec) {
+ Preconditions.checkNotNull(suggestionQueryExpression);
+ Preconditions.checkNotNull(searchSuggestionSpec);
+ if (Build.VERSION.SDK_INT >= 34) {
+ ResolvableFuture<List<SearchSuggestionResult>> future = ResolvableFuture.create();
+ ApiHelperForU.searchSuggestion(
+ mPlatformSession,
+ suggestionQueryExpression,
+ SearchSuggestionSpecToPlatformConverter
+ .toPlatformSearchSuggestionSpec(searchSuggestionSpec),
+ mExecutor,
+ result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+ result,
+ future,
+ SearchSuggestionResultToPlatformConverter
+ ::toJetpackSearchSuggestionResults));
+ return future;
+ } else {
+ throw new UnsupportedOperationException(
+ "Search Suggestion is not supported on this AppSearch implementation.");
+ }
}
@Override
@@ -195,9 +229,11 @@
return future;
}
+ // TODO(b/265311462): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastU() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@Override
@NonNull
- @BuildCompat.PrereleaseSdkCheck
public ListenableFuture<Void> removeAsync(
@NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
Preconditions.checkNotNull(queryExpression);
@@ -295,4 +331,23 @@
public void close() {
mPlatformSession.close();
}
+
+ @RequiresApi(34)
+ static class ApiHelperForU {
+ private ApiHelperForU() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static void searchSuggestion(
+ @NonNull android.app.appsearch.AppSearchSession appSearchSession,
+ @NonNull String suggestionQueryExpression,
+ @NonNull android.app.appsearch.SearchSuggestionSpec searchSuggestionSpec,
+ @NonNull Executor executor,
+ @NonNull Consumer<AppSearchResult<
+ List<android.app.appsearch.SearchSuggestionResult>>> callback) {
+ appSearchSession.searchSuggestion(suggestionQueryExpression, searchSuggestionSpec,
+ executor, callback);
+ }
+ }
}
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 05575ce..27b1c19 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
@@ -18,6 +18,7 @@
import android.os.Build;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
@@ -43,8 +44,10 @@
* Translates a platform {@link android.app.appsearch.GetSchemaResponse} into a jetpack
* {@link GetSchemaResponse}.
*/
- @NonNull
+ // TODO(b/265311462): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastU() is removed.
@BuildCompat.PrereleaseSdkCheck
+ @NonNull
public static GetSchemaResponse toJetpackGetSchemaResponse(
@NonNull android.app.appsearch.GetSchemaResponse platformResponse) {
Preconditions.checkNotNull(platformResponse);
@@ -60,17 +63,18 @@
jetpackBuilder.addSchema(SchemaToPlatformConverter.toJetpackSchema(platformSchema));
}
jetpackBuilder.setVersion(platformResponse.getVersion());
- if (BuildCompat.isAtLeastT()) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// Convert schemas not displayed by system
for (String schemaTypeNotDisplayedBySystem :
- platformResponse.getSchemaTypesNotDisplayedBySystem()) {
+ ApiHelperForT.getSchemaTypesNotDisplayedBySystem(platformResponse)) {
jetpackBuilder.addSchemaTypeNotDisplayedBySystem(schemaTypeNotDisplayedBySystem);
}
// Convert schemas visible to packages
convertSchemasVisibleToPackages(platformResponse, jetpackBuilder);
// Convert schemas visible to permissions
for (Map.Entry<String, Set<Set<Integer>>> entry :
- platformResponse.getRequiredPermissionsForSchemaTypeVisibility().entrySet()) {
+ ApiHelperForT.getRequiredPermissionsForSchemaTypeVisibility(platformResponse)
+ .entrySet()) {
jetpackBuilder.setRequiredPermissionsForSchemaTypeVisibility(entry.getKey(),
entry.getValue());
}
@@ -90,7 +94,7 @@
// incorrectly returns {@code null} in some prerelease versions of Android T. Remove
// this workaround after the issue is fixed in T.
Map<String, Set<android.app.appsearch.PackageIdentifier>> schemaTypesVisibleToPackages =
- platformResponse.getSchemaTypesVisibleToPackages();
+ ApiHelperForT.getSchemaTypesVisibleToPackage(platformResponse);
if (schemaTypesVisibleToPackages != null) {
for (Map.Entry<String, Set<android.app.appsearch.PackageIdentifier>> entry
: schemaTypesVisibleToPackages.entrySet()) {
@@ -107,4 +111,30 @@
}
}
}
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private static class ApiHelperForT {
+ private ApiHelperForT() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static Set<String> getSchemaTypesNotDisplayedBySystem(
+ android.app.appsearch.GetSchemaResponse platformResponse) {
+ return platformResponse.getSchemaTypesNotDisplayedBySystem();
+ }
+
+ @DoNotInline
+ static Map<String, Set<android.app.appsearch.PackageIdentifier>>
+ getSchemaTypesVisibleToPackage(
+ android.app.appsearch.GetSchemaResponse platformResponse) {
+ return platformResponse.getSchemaTypesVisibleToPackages();
+ }
+
+ @DoNotInline
+ static Map<String, Set<Set<Integer>>> getRequiredPermissionsForSchemaTypeVisibility(
+ android.app.appsearch.GetSchemaResponse platformResponse) {
+ return platformResponse.getRequiredPermissionsForSchemaTypeVisibility();
+ }
+ }
}
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
new file mode 100644
index 0000000..9696cec
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/JoinSpecToPlatformConverter.java
@@ -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.appsearch.platformstorage.converter;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.JoinSpec;
+import androidx.core.os.BuildCompat;
+import androidx.core.util.Preconditions;
+
+/**
+ * Translates between Platform and Jetpack versions of {@link JoinSpec}.
+ */
+// TODO(b/265311462): Remove BuildCompat.PrereleaseSdkCheck annotation once
+// SearchSpecToPlatformConverter.toPlatformSearchSpec() removes it. Also, replace literal '34' with
+// Build.VERSION_CODES.UPSIDE_DOWN_CAKE once the SDK_INT is finalized.
+@BuildCompat.PrereleaseSdkCheck
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@RequiresApi(34)
+public class JoinSpecToPlatformConverter {
+ private JoinSpecToPlatformConverter() {}
+
+ /**
+ * Translates a Jetpack {@link JoinSpec} into a platform {@link android.app.appsearch.JoinSpec}.
+ */
+ @SuppressLint("WrongConstant")
+ @NonNull
+ public static android.app.appsearch.JoinSpec toPlatformJoinSpec(@NonNull JoinSpec jetpackSpec) {
+ Preconditions.checkNotNull(jetpackSpec);
+ return new android.app.appsearch.JoinSpec.Builder(jetpackSpec.getChildPropertyExpression())
+ .setNestedSearch(
+ jetpackSpec.getNestedQuery(),
+ SearchSpecToPlatformConverter.toPlatformSearchSpec(
+ jetpackSpec.getNestedSearchSpec()))
+ .setMaxJoinedResultCount(jetpackSpec.getMaxJoinedResultCount())
+ .setAggregationScoringStrategy(jetpackSpec.getAggregationScoringStrategy())
+ .build();
+ }
+}
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 eba43cf..9015bef 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
@@ -19,16 +19,18 @@
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.AppSearchSchema;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Preconditions;
import java.util.List;
/**
- * Translates a jetpack {@link androidx.appsearch.app.AppSearchSchema} into a platform
+ * Translates a jetpack {@link AppSearchSchema} into a platform
* {@link android.app.appsearch.AppSearchSchema}.
* @hide
*/
@@ -38,9 +40,12 @@
private SchemaToPlatformConverter() {}
/**
- * Translates a jetpack {@link androidx.appsearch.app.AppSearchSchema} into a platform
+ * Translates a jetpack {@link AppSearchSchema} into a platform
* {@link android.app.appsearch.AppSearchSchema}.
*/
+ // TODO(b/265311462): Remove BuildCompat.PrereleaseSdkCheck annotation once
+ // toPlatformProperty() doesn't have it either.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
public static android.app.appsearch.AppSearchSchema toPlatformSchema(
@NonNull AppSearchSchema jetpackSchema) {
@@ -58,8 +63,11 @@
/**
* Translates a platform {@link android.app.appsearch.AppSearchSchema} to a jetpack
- * {@link androidx.appsearch.app.AppSearchSchema}.
+ * {@link AppSearchSchema}.
*/
+ // TODO(b/265311462): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastU() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
public static AppSearchSchema toJetpackSchema(
@NonNull android.app.appsearch.AppSearchSchema platformSchema) {
@@ -78,6 +86,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/265311462): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastU() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
private static android.app.appsearch.AppSearchSchema.PropertyConfig toPlatformProperty(
@NonNull AppSearchSchema.PropertyConfig jetpackProperty) {
@@ -85,34 +96,47 @@
if (jetpackProperty instanceof AppSearchSchema.StringPropertyConfig) {
AppSearchSchema.StringPropertyConfig stringProperty =
(AppSearchSchema.StringPropertyConfig) jetpackProperty;
- // TODO(b/256022027): add isAtLeastU check to allow JOINABLE_VALUE_TYPE_QUALIFIED_ID
- // after Android U, and set joinable value type to PropertyConfig.
- if (stringProperty.getJoinableValueType()
- == AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) {
- throw new UnsupportedOperationException(
- "StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID is not supported on "
- + "this AppSearch implementation.");
- }
- return new android.app.appsearch.AppSearchSchema.StringPropertyConfig.Builder(
+ android.app.appsearch.AppSearchSchema.StringPropertyConfig.Builder platformBuilder =
+ new android.app.appsearch.AppSearchSchema.StringPropertyConfig.Builder(
stringProperty.getName())
.setCardinality(stringProperty.getCardinality())
.setIndexingType(stringProperty.getIndexingType())
- .setTokenizerType(stringProperty.getTokenizerType())
- .build();
+ .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) {
+ if (!BuildCompat.isAtLeastU()) {
+ throw new UnsupportedOperationException(
+ "StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID is not supported"
+ + " on this AppSearch implementation.");
+ }
+ ApiHelperForU.setJoinableValueType(platformBuilder,
+ stringProperty.getJoinableValueType());
+ }
+ return platformBuilder.build();
} else if (jetpackProperty instanceof AppSearchSchema.LongPropertyConfig) {
AppSearchSchema.LongPropertyConfig longProperty =
(AppSearchSchema.LongPropertyConfig) jetpackProperty;
- // TODO(b/259744228): add isAtLeastU check to allow INDEXING_TYPE_RANGE after Android U.
+ android.app.appsearch.AppSearchSchema.LongPropertyConfig.Builder longPropertyBuilder =
+ new android.app.appsearch.AppSearchSchema.LongPropertyConfig.Builder(
+ jetpackProperty.getName())
+ .setCardinality(jetpackProperty.getCardinality());
if (longProperty.getIndexingType()
== AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE) {
- throw new UnsupportedOperationException(
- "LongProperty.INDEXING_TYPE_RANGE is not supported on this AppSearch "
- + "implementation.");
+ if (!BuildCompat.isAtLeastU()) {
+ throw new UnsupportedOperationException(
+ "LongProperty.INDEXING_TYPE_RANGE is not supported on this AppSearch "
+ + "implementation.");
+ }
+ ApiHelperForU.setIndexingType(
+ longPropertyBuilder, longProperty.getIndexingType());
}
- return new android.app.appsearch.AppSearchSchema.LongPropertyConfig.Builder(
- jetpackProperty.getName())
- .setCardinality(jetpackProperty.getCardinality())
- .build();
+ return longPropertyBuilder.build();
} else if (jetpackProperty instanceof AppSearchSchema.DoublePropertyConfig) {
return new android.app.appsearch.AppSearchSchema.DoublePropertyConfig.Builder(
jetpackProperty.getName())
@@ -145,6 +169,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/265311462): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastU() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
private static AppSearchSchema.PropertyConfig toJetpackProperty(
@NonNull android.app.appsearch.AppSearchSchema.PropertyConfig platformProperty) {
@@ -153,16 +180,28 @@
instanceof android.app.appsearch.AppSearchSchema.StringPropertyConfig) {
android.app.appsearch.AppSearchSchema.StringPropertyConfig stringProperty =
(android.app.appsearch.AppSearchSchema.StringPropertyConfig) platformProperty;
- return new AppSearchSchema.StringPropertyConfig.Builder(stringProperty.getName())
- .setCardinality(stringProperty.getCardinality())
- .setIndexingType(stringProperty.getIndexingType())
- .setTokenizerType(stringProperty.getTokenizerType())
- .build();
+ AppSearchSchema.StringPropertyConfig.Builder jetpackBuilder =
+ new AppSearchSchema.StringPropertyConfig.Builder(stringProperty.getName())
+ .setCardinality(stringProperty.getCardinality())
+ .setIndexingType(stringProperty.getIndexingType())
+ .setTokenizerType(stringProperty.getTokenizerType());
+ if (BuildCompat.isAtLeastU()) {
+ jetpackBuilder.setJoinableValueType(
+ ApiHelperForU.getJoinableValueType(stringProperty));
+ }
+ return jetpackBuilder.build();
} else if (platformProperty
instanceof android.app.appsearch.AppSearchSchema.LongPropertyConfig) {
- return new AppSearchSchema.LongPropertyConfig.Builder(platformProperty.getName())
- .setCardinality(platformProperty.getCardinality())
- .build();
+ android.app.appsearch.AppSearchSchema.LongPropertyConfig longProperty =
+ (android.app.appsearch.AppSearchSchema.LongPropertyConfig) platformProperty;
+ AppSearchSchema.LongPropertyConfig.Builder jetpackBuilder =
+ new AppSearchSchema.LongPropertyConfig.Builder(longProperty.getName())
+ .setCardinality(longProperty.getCardinality());
+ if (BuildCompat.isAtLeastU()) {
+ jetpackBuilder.setIndexingType(
+ ApiHelperForU.getIndexingType(longProperty));
+ }
+ return jetpackBuilder.build();
} else if (platformProperty
instanceof android.app.appsearch.AppSearchSchema.DoublePropertyConfig) {
return new AppSearchSchema.DoublePropertyConfig.Builder(platformProperty.getName())
@@ -194,4 +233,47 @@
+ ": " + platformProperty);
}
}
+
+ // TODO(b/265311462): Replace literal '34' with Build.VERSION_CODES.UPSIDE_DOWN_CAKE when the
+ // SDK_INT is finalized.
+ @RequiresApi(34)
+ private static class ApiHelperForU {
+ private ApiHelperForU() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static void setJoinableValueType(
+ android.app.appsearch.AppSearchSchema.StringPropertyConfig.Builder builder,
+ @AppSearchSchema.StringPropertyConfig.JoinableValueType int joinableValueType) {
+ builder.setJoinableValueType(joinableValueType);
+ }
+
+ // 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")
+ @DoNotInline
+ @AppSearchSchema.StringPropertyConfig.JoinableValueType
+ static int getJoinableValueType(
+ android.app.appsearch.AppSearchSchema.StringPropertyConfig stringPropertyConfig) {
+ return stringPropertyConfig.getJoinableValueType();
+ }
+
+ @DoNotInline
+ static void setIndexingType(
+ android.app.appsearch.AppSearchSchema.LongPropertyConfig.Builder builder,
+ @AppSearchSchema.LongPropertyConfig.IndexingType int longIndexingType) {
+ builder.setIndexingType(longIndexingType);
+ }
+
+ // Most LongProperty.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")
+ @DoNotInline
+ @AppSearchSchema.LongPropertyConfig.IndexingType
+ static int getIndexingType(
+ android.app.appsearch.AppSearchSchema.LongPropertyConfig longPropertyConfig) {
+ return longPropertyConfig.getIndexingType();
+ }
+ }
}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java
index 707234d..a36e9cf 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java
@@ -18,6 +18,7 @@
import android.os.Build;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
@@ -38,6 +39,8 @@
private SearchResultToPlatformConverter() {}
/** Translates from Platform to Jetpack versions of {@link SearchResult}. */
+ // TODO(b/265311462): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastU() is removed.
@BuildCompat.PrereleaseSdkCheck
@NonNull
public static SearchResult toJetpackSearchResult(
@@ -55,10 +58,15 @@
SearchResult.MatchInfo jetpackMatchInfo = toJetpackMatchInfo(platformMatches.get(i));
builder.addMatchInfo(jetpackMatchInfo);
}
+ if (BuildCompat.isAtLeastU()) {
+ for (android.app.appsearch.SearchResult joinedResult :
+ ApiHelperForU.getJoinedResults(platformResult)) {
+ builder.addJoinedResult(toJetpackSearchResult(joinedResult));
+ }
+ }
return builder.build();
}
- @BuildCompat.PrereleaseSdkCheck
@NonNull
private static SearchResult.MatchInfo toJetpackMatchInfo(
@NonNull android.app.appsearch.SearchResult.MatchInfo platformMatchInfo) {
@@ -73,12 +81,46 @@
new SearchResult.MatchRange(
platformMatchInfo.getSnippetRange().getStart(),
platformMatchInfo.getSnippetRange().getEnd()));
- if (BuildCompat.isAtLeastT()) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
builder.setSubmatchRange(
new SearchResult.MatchRange(
- platformMatchInfo.getSubmatchRange().getStart(),
- platformMatchInfo.getSubmatchRange().getEnd()));
+ ApiHelperForT.getSubmatchRangeStart(platformMatchInfo),
+ ApiHelperForT.getSubmatchRangeEnd(platformMatchInfo)));
}
return builder.build();
}
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private static class ApiHelperForT {
+ private ApiHelperForT() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static int getSubmatchRangeStart(@NonNull
+ android.app.appsearch.SearchResult.MatchInfo platformMatchInfo) {
+ return platformMatchInfo.getSubmatchRange().getStart();
+ }
+
+ @DoNotInline
+ static int getSubmatchRangeEnd(@NonNull
+ android.app.appsearch.SearchResult.MatchInfo platformMatchInfo) {
+ return platformMatchInfo.getSubmatchRange().getEnd();
+ }
+ }
+
+ // TODO(b/265311462): Replace literal '34' with Build.VERSION_CODES.UPSIDE_DOWN_CAKE when the
+ // SDK_INT is finalized.
+ @RequiresApi(34)
+ private static class ApiHelperForU {
+ private ApiHelperForU() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static List<android.app.appsearch.SearchResult> getJoinedResults(@NonNull
+ android.app.appsearch.SearchResult result) {
+ return result.getJoinedResults();
+ }
+ }
}
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 cc2bd0a..ea743a0 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
@@ -19,11 +19,14 @@
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.JoinSpec;
import androidx.appsearch.app.SearchSpec;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Preconditions;
import java.util.List;
@@ -44,35 +47,49 @@
// 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/265311462): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastU() is removed.
+ @BuildCompat.PrereleaseSdkCheck
@NonNull
public static android.app.appsearch.SearchSpec toPlatformSearchSpec(
@NonNull SearchSpec jetpackSearchSpec) {
Preconditions.checkNotNull(jetpackSearchSpec);
- if (!jetpackSearchSpec.getAdvancedRankingExpression().isEmpty()) {
- // TODO(b/261474063): Remove this once advanced ranking becomes available.
- throw new UnsupportedOperationException(
- Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION
- + " is not available on this AppSearch implementation.");
- }
-
android.app.appsearch.SearchSpec.Builder platformBuilder =
new android.app.appsearch.SearchSpec.Builder();
+ if (!jetpackSearchSpec.getAdvancedRankingExpression().isEmpty()) {
+ if (!BuildCompat.isAtLeastU()) {
+ throw new UnsupportedOperationException(
+ Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION
+ + " is not available on this AppSearch implementation.");
+ }
+ ApiHelperForU.setRankingStrategy(
+ platformBuilder, jetpackSearchSpec.getAdvancedRankingExpression());
+ } else {
+ platformBuilder.setRankingStrategy(jetpackSearchSpec.getRankingStrategy());
+ }
+
platformBuilder
.setTermMatch(jetpackSearchSpec.getTermMatch())
.addFilterSchemas(jetpackSearchSpec.getFilterSchemas())
.addFilterNamespaces(jetpackSearchSpec.getFilterNamespaces())
.addFilterPackageNames(jetpackSearchSpec.getFilterPackageNames())
.setResultCountPerPage(jetpackSearchSpec.getResultCountPerPage())
- .setRankingStrategy(jetpackSearchSpec.getRankingStrategy())
.setOrder(jetpackSearchSpec.getOrder())
.setSnippetCount(jetpackSearchSpec.getSnippetCount())
.setSnippetCountPerProperty(jetpackSearchSpec.getSnippetCountPerProperty())
.setMaxSnippetSize(jetpackSearchSpec.getMaxSnippetSize());
- //TODO(b/262512396): add the enabledFeatures set from the SearchSpec once it is synced
- // across to platform.
if (jetpackSearchSpec.getResultGroupingTypeFlags() != 0) {
+ // Feature SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is only supported on Android U.
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
+ if ((jetpackSearchSpec.getResultGroupingTypeFlags()
+ & SearchSpec.GROUPING_TYPE_PER_SCHEMA) != 0) {
+ throw new UnsupportedOperationException(
+ Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
+ + " is not available on this AppSearch implementation.");
+ }
+ }
platformBuilder.setResultGrouping(
jetpackSearchSpec.getResultGroupingTypeFlags(),
jetpackSearchSpec.getResultGroupingLimit());
@@ -82,13 +99,82 @@
platformBuilder.addProjection(projection.getKey(), projection.getValue());
}
- // TODO(b/203700301) : Update to reflect support in Android U+ once this
- // feature is synced over into service-appsearch.
if (!jetpackSearchSpec.getPropertyWeights().isEmpty()) {
- throw new UnsupportedOperationException(
- "Property weights are not supported with this backend/Android API level "
- + "combination.");
+ if (!BuildCompat.isAtLeastU()) {
+ throw new UnsupportedOperationException(
+ "Property weights are not supported with this backend/Android API level "
+ + "combination.");
+ }
+ ApiHelperForU.setPropertyWeights(platformBuilder,
+ jetpackSearchSpec.getPropertyWeights());
+ }
+
+ if (!jetpackSearchSpec.getEnabledFeatures().isEmpty()) {
+ if (jetpackSearchSpec.isNumericSearchEnabled()
+ || jetpackSearchSpec.isVerbatimSearchEnabled()
+ || jetpackSearchSpec.isListFilterQueryLanguageEnabled()) {
+ if (!BuildCompat.isAtLeastU()) {
+ 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.");
+ }
+ ApiHelperForU.copyEnabledFeatures(platformBuilder, jetpackSearchSpec);
+ }
+ }
+
+ if (jetpackSearchSpec.getJoinSpec() != null) {
+ if (!BuildCompat.isAtLeastU()) {
+ throw new UnsupportedOperationException("JoinSpec is not available on this "
+ + "AppSearch implementation.");
+ }
+ ApiHelperForU.setJoinSpec(platformBuilder, jetpackSearchSpec.getJoinSpec());
}
return platformBuilder.build();
}
+
+ // TODO(b/265311462): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastU() is removed. Also, replace literal '34' with
+ // Build.VERSION_CODES.UPSIDE_DOWN_CAKE once the SDK_INT is finalized.
+ @BuildCompat.PrereleaseSdkCheck
+ @RequiresApi(34)
+ private static class ApiHelperForU {
+ private ApiHelperForU() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static void setJoinSpec(@NonNull android.app.appsearch.SearchSpec.Builder builder,
+ JoinSpec jetpackJoinSpec) {
+ builder.setJoinSpec(JoinSpecToPlatformConverter.toPlatformJoinSpec(jetpackJoinSpec));
+ }
+
+ @DoNotInline
+ static void setRankingStrategy(@NonNull android.app.appsearch.SearchSpec.Builder builder,
+ @NonNull String rankingExpression) {
+ builder.setRankingStrategy(rankingExpression);
+ }
+
+ @DoNotInline
+ static void copyEnabledFeatures(@NonNull android.app.appsearch.SearchSpec.Builder builder,
+ @NonNull SearchSpec jetpackSpec) {
+ if (jetpackSpec.isNumericSearchEnabled()) {
+ builder.setNumericSearchEnabled(true);
+ }
+ if (jetpackSpec.isVerbatimSearchEnabled()) {
+ builder.setVerbatimSearchEnabled(true);
+ }
+ if (jetpackSpec.isListFilterQueryLanguageEnabled()) {
+ builder.setListFilterQueryLanguageEnabled(true);
+ }
+ }
+
+ @DoNotInline
+ static void setPropertyWeights(@NonNull android.app.appsearch.SearchSpec.Builder builder,
+ @NonNull Map<String, Map<String, Double>> propertyWeightsMap) {
+ for (Map.Entry<String, Map<String, Double>> entry : propertyWeightsMap.entrySet()) {
+ builder.setPropertyWeights(entry.getKey(), entry.getValue());
+ }
+ }
+ }
}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionResultToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionResultToPlatformConverter.java
new file mode 100644
index 0000000..beebb35
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionResultToPlatformConverter.java
@@ -0,0 +1,54 @@
+/*
+ * 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.platformstorage.converter;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.SearchSuggestionResult;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Translates between Platform and Jetpack versions of {@link SearchSuggestionResult}.
+ *
+ * @hide
+ */
+// TODO(b/227356108) replace literal '34' with Build.VERSION_CODES.U once the SDK_INT is finalized.
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(34)
+public class SearchSuggestionResultToPlatformConverter {
+ private SearchSuggestionResultToPlatformConverter() {}
+
+ /** Translates from Platform to Jetpack versions of {@linkSearchSuggestionResult} */
+ @NonNull
+ public static List<SearchSuggestionResult> toJetpackSearchSuggestionResults(
+ @NonNull List<android.app.appsearch.SearchSuggestionResult>
+ platformSearchSuggestionResults) {
+ Preconditions.checkNotNull(platformSearchSuggestionResults);
+ List<SearchSuggestionResult> jetpackSearchSuggestionResults =
+ new ArrayList<>(platformSearchSuggestionResults.size());
+ for (int i = 0; i < platformSearchSuggestionResults.size(); i++) {
+ jetpackSearchSuggestionResults.add(new SearchSuggestionResult.Builder()
+ .setSuggestedResult(platformSearchSuggestionResults.get(i).getSuggestedResult())
+ .build());
+ }
+ return jetpackSearchSuggestionResults;
+ }
+}
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
new file mode 100644
index 0000000..86f20af
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionSpecToPlatformConverter.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.platformstorage.converter;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.SearchSuggestionSpec;
+import androidx.core.util.Preconditions;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Translates between Platform and Jetpack versions of {@link SearchSuggestionSpec}.
+ *
+ * @hide
+ */
+// TODO(b/227356108) replace literal '34' with Build.VERSION_CODES.U once the SDK_INT is finalized.
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(34)
+public final class SearchSuggestionSpecToPlatformConverter {
+ private SearchSuggestionSpecToPlatformConverter() {
+ }
+
+ /** Translates from Jetpack to Platform 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 android.app.appsearch.SearchSuggestionSpec toPlatformSearchSuggestionSpec(
+ @NonNull SearchSuggestionSpec jetpackSearchSuggestionSpec) {
+ Preconditions.checkNotNull(jetpackSearchSuggestionSpec);
+
+ android.app.appsearch.SearchSuggestionSpec.Builder platformBuilder =
+ new android.app.appsearch.SearchSuggestionSpec.Builder(
+ jetpackSearchSuggestionSpec.getMaximumResultCount());
+
+ platformBuilder
+ .addFilterNamespaces(jetpackSearchSuggestionSpec.getFilterNamespaces())
+ .addFilterSchemas(jetpackSearchSuggestionSpec.getFilterSchemas())
+ .setRankingStrategy(jetpackSearchSuggestionSpec.getRankingStrategy());
+ for (Map.Entry<String, List<String>> documentIdFilters :
+ jetpackSearchSuggestionSpec.getFilterDocumentIds().entrySet()) {
+ platformBuilder.addFilterDocumentIds(documentIdFilters.getKey(),
+ documentIdFilters.getValue());
+ }
+ return platformBuilder.build();
+ }
+}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java
index 48c3ede..0ca4dac 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
@@ -18,6 +18,7 @@
import android.os.Build;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
@@ -47,6 +48,8 @@
* Translates a jetpack {@link SetSchemaRequest} into a platform
* {@link android.app.appsearch.SetSchemaRequest}.
*/
+ // TODO(b/265311462): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+ // BuildCompat.isAtLeastU() is removed.
@BuildCompat.PrereleaseSdkCheck
@NonNull
public static android.app.appsearch.SetSchemaRequest toPlatformSetSchemaRequest(
@@ -82,7 +85,7 @@
for (Map.Entry<String, Set<Set<Integer>>> entry :
jetpackRequest.getRequiredPermissionsForSchemaTypeVisibility().entrySet()) {
for (Set<Integer> permissionGroup : entry.getValue()) {
- platformBuilder.addRequiredPermissionsForSchemaTypeVisibility(
+ ApiHelperForT.addRequiredPermissionsForSchemaTypeVisibility(platformBuilder,
entry.getKey(), permissionGroup);
}
}
@@ -163,4 +166,18 @@
}
return jetpackBuilder.build();
}
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private static class ApiHelperForT {
+ private ApiHelperForT() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static void addRequiredPermissionsForSchemaTypeVisibility(
+ android.app.appsearch.SetSchemaRequest.Builder platformBuilder,
+ String schemaType, Set<Integer> permissions) {
+ platformBuilder.addRequiredPermissionsForSchemaTypeVisibility(schemaType, permissions);
+ }
+ }
}
diff --git a/appsearch/appsearch-play-services-storage/api/current.txt b/appsearch/appsearch-play-services-storage/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appsearch/appsearch-play-services-storage/api/public_plus_experimental_current.txt b/appsearch/appsearch-play-services-storage/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/appsearch/appsearch-play-services-storage/api/res-current.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to appsearch/appsearch-play-services-storage/api/res-current.txt
diff --git a/appsearch/appsearch-play-services-storage/api/restricted_current.txt b/appsearch/appsearch-play-services-storage/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appsearch/appsearch-play-services-storage/build.gradle b/appsearch/appsearch-play-services-storage/build.gradle
new file mode 100644
index 0000000..bcbac5e
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/build.gradle
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+}
+
+dependencies {
+ implementation project(":appsearch:appsearch")
+}
+
+androidx {
+ name = "AppSearch Play Services Storage"
+ publish = Publish.SNAPSHOT_AND_RELEASE
+ inceptionYear = "2023"
+ description =
+ "An implementation of AppSearchSession and GlobalSearchSession on pre-S devices using " +
+ "play-services-appsearch SDK with Gogle Play Services as storage backend."
+}
+
+android {
+ namespace "androidx.appsearch.playservicesstorage"
+}
diff --git a/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/AppSearchEmail.java b/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/AppSearchEmail.java
index 5aec156..08b1840 100644
--- a/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/AppSearchEmail.java
+++ b/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/AppSearchEmail.java
@@ -19,6 +19,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.AppSearchSchema.PropertyConfig;
import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
@@ -170,6 +171,7 @@
/**
* Sets the from address of {@link AppSearchEmail}
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setFrom(@NonNull String from) {
return setPropertyString(KEY_FROM, from);
@@ -178,6 +180,7 @@
/**
* Sets the destination address of {@link AppSearchEmail}
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setTo(@NonNull String... to) {
return setPropertyString(KEY_TO, to);
@@ -186,6 +189,7 @@
/**
* Sets the CC list of {@link AppSearchEmail}
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setCc(@NonNull String... cc) {
return setPropertyString(KEY_CC, cc);
@@ -194,6 +198,7 @@
/**
* Sets the BCC list of {@link AppSearchEmail}
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setBcc(@NonNull String... bcc) {
return setPropertyString(KEY_BCC, bcc);
@@ -202,6 +207,7 @@
/**
* Sets the subject of {@link AppSearchEmail}
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setSubject(@NonNull String subject) {
return setPropertyString(KEY_SUBJECT, subject);
@@ -210,6 +216,7 @@
/**
* Sets the body of {@link AppSearchEmail}
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setBody(@NonNull String body) {
return setPropertyString(KEY_BODY, body);
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index 289cdbf..f2dc046 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -46,6 +46,7 @@
@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.StringProperty {
method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
+ method public abstract int joinableValueType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE;
method public abstract String name() default "";
method public abstract boolean required() default false;
method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
@@ -80,6 +81,7 @@
method public boolean isSuccess();
method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newFailedResult(int, String?);
method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newSuccessfulResult(ValueType?);
+ field public static final int RESULT_DENIED = 9; // 0x9
field public static final int RESULT_INTERNAL_ERROR = 2; // 0x2
field public static final int RESULT_INVALID_ARGUMENT = 3; // 0x3
field public static final int RESULT_INVALID_SCHEMA = 7; // 0x7
@@ -163,6 +165,7 @@
}
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();
@@ -181,6 +184,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 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);
@@ -199,11 +203,13 @@
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsageAsync(androidx.appsearch.app.ReportUsageRequest);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlushAsync();
method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+ method public com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchSuggestionResult!>!> searchSuggestionAsync(String, androidx.appsearch.app.SearchSuggestionSpec);
method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchemaAsync(androidx.appsearch.app.SetSchemaRequest);
}
public interface DocumentClassFactory<T> {
method public T fromGenericDocument(androidx.appsearch.app.GenericDocument) throws androidx.appsearch.exceptions.AppSearchException;
+ method public java.util.List<java.lang.Class<?>!> getNestedDocumentClasses() throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.AppSearchSchema getSchema() throws androidx.appsearch.exceptions.AppSearchException;
method public String getSchemaName();
method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
@@ -218,9 +224,12 @@
field public static final String JOIN_SPEC_AND_QUALIFIED_ID = "JOIN_SPEC_AND_QUALIFIED_ID";
field public static final String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
field public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH";
+ field public static final String SCHEMA_SET_DELETION_PROPAGATION = "SCHEMA_SET_DELETION_PROPAGATION";
field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
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_SUGGESTION = "SEARCH_SUGGESTION";
field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
field public static final String VERBATIM_SEARCH = "VERBATIM_SEARCH";
}
@@ -329,7 +338,6 @@
field public static final int AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL = 0; // 0x0
field public static final int AGGREGATION_SCORING_RESULT_COUNT = 1; // 0x1
field public static final int AGGREGATION_SCORING_SUM_RANKING_SIGNAL = 5; // 0x5
- field public static final String QUALIFIED_ID = "this.qualifiedId()";
}
public static final class JoinSpec.Builder {
@@ -495,6 +503,7 @@
method public boolean isVerbatimSearchEnabled();
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 = "*";
@@ -546,7 +555,7 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.VERBATIM_SEARCH) public androidx.appsearch.app.SearchSpec.Builder setVerbatimSearchEnabled(boolean);
}
- public class SearchSuggestionResult {
+ public final class SearchSuggestionResult {
method public String getSuggestedResult();
}
@@ -556,7 +565,7 @@
method public androidx.appsearch.app.SearchSuggestionResult.Builder setSuggestedResult(String);
}
- public class SearchSuggestionSpec {
+ 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.List<java.lang.String!> getFilterSchemas();
@@ -577,7 +586,7 @@
method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
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() throws androidx.appsearch.exceptions.AppSearchException;
+ method public androidx.appsearch.app.SearchSuggestionSpec build();
method public androidx.appsearch.app.SearchSuggestionSpec.Builder setRankingStrategy(int);
}
diff --git a/appsearch/appsearch/api/public_plus_experimental_current.txt b/appsearch/appsearch/api/public_plus_experimental_current.txt
index 289cdbf..f2dc046 100644
--- a/appsearch/appsearch/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch/api/public_plus_experimental_current.txt
@@ -46,6 +46,7 @@
@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.StringProperty {
method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
+ method public abstract int joinableValueType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE;
method public abstract String name() default "";
method public abstract boolean required() default false;
method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
@@ -80,6 +81,7 @@
method public boolean isSuccess();
method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newFailedResult(int, String?);
method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newSuccessfulResult(ValueType?);
+ field public static final int RESULT_DENIED = 9; // 0x9
field public static final int RESULT_INTERNAL_ERROR = 2; // 0x2
field public static final int RESULT_INVALID_ARGUMENT = 3; // 0x3
field public static final int RESULT_INVALID_SCHEMA = 7; // 0x7
@@ -163,6 +165,7 @@
}
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();
@@ -181,6 +184,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 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);
@@ -199,11 +203,13 @@
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsageAsync(androidx.appsearch.app.ReportUsageRequest);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlushAsync();
method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+ method public com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchSuggestionResult!>!> searchSuggestionAsync(String, androidx.appsearch.app.SearchSuggestionSpec);
method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchemaAsync(androidx.appsearch.app.SetSchemaRequest);
}
public interface DocumentClassFactory<T> {
method public T fromGenericDocument(androidx.appsearch.app.GenericDocument) throws androidx.appsearch.exceptions.AppSearchException;
+ method public java.util.List<java.lang.Class<?>!> getNestedDocumentClasses() throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.AppSearchSchema getSchema() throws androidx.appsearch.exceptions.AppSearchException;
method public String getSchemaName();
method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
@@ -218,9 +224,12 @@
field public static final String JOIN_SPEC_AND_QUALIFIED_ID = "JOIN_SPEC_AND_QUALIFIED_ID";
field public static final String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
field public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH";
+ field public static final String SCHEMA_SET_DELETION_PROPAGATION = "SCHEMA_SET_DELETION_PROPAGATION";
field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
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_SUGGESTION = "SEARCH_SUGGESTION";
field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
field public static final String VERBATIM_SEARCH = "VERBATIM_SEARCH";
}
@@ -329,7 +338,6 @@
field public static final int AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL = 0; // 0x0
field public static final int AGGREGATION_SCORING_RESULT_COUNT = 1; // 0x1
field public static final int AGGREGATION_SCORING_SUM_RANKING_SIGNAL = 5; // 0x5
- field public static final String QUALIFIED_ID = "this.qualifiedId()";
}
public static final class JoinSpec.Builder {
@@ -495,6 +503,7 @@
method public boolean isVerbatimSearchEnabled();
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 = "*";
@@ -546,7 +555,7 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.VERBATIM_SEARCH) public androidx.appsearch.app.SearchSpec.Builder setVerbatimSearchEnabled(boolean);
}
- public class SearchSuggestionResult {
+ public final class SearchSuggestionResult {
method public String getSuggestedResult();
}
@@ -556,7 +565,7 @@
method public androidx.appsearch.app.SearchSuggestionResult.Builder setSuggestedResult(String);
}
- public class SearchSuggestionSpec {
+ 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.List<java.lang.String!> getFilterSchemas();
@@ -577,7 +586,7 @@
method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
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() throws androidx.appsearch.exceptions.AppSearchException;
+ method public androidx.appsearch.app.SearchSuggestionSpec build();
method public androidx.appsearch.app.SearchSuggestionSpec.Builder setRankingStrategy(int);
}
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index 289cdbf..f2dc046 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -46,6 +46,7 @@
@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.StringProperty {
method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
+ method public abstract int joinableValueType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE;
method public abstract String name() default "";
method public abstract boolean required() default false;
method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
@@ -80,6 +81,7 @@
method public boolean isSuccess();
method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newFailedResult(int, String?);
method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newSuccessfulResult(ValueType?);
+ field public static final int RESULT_DENIED = 9; // 0x9
field public static final int RESULT_INTERNAL_ERROR = 2; // 0x2
field public static final int RESULT_INVALID_ARGUMENT = 3; // 0x3
field public static final int RESULT_INVALID_SCHEMA = 7; // 0x7
@@ -163,6 +165,7 @@
}
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();
@@ -181,6 +184,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 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);
@@ -199,11 +203,13 @@
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsageAsync(androidx.appsearch.app.ReportUsageRequest);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlushAsync();
method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+ method public com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchSuggestionResult!>!> searchSuggestionAsync(String, androidx.appsearch.app.SearchSuggestionSpec);
method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchemaAsync(androidx.appsearch.app.SetSchemaRequest);
}
public interface DocumentClassFactory<T> {
method public T fromGenericDocument(androidx.appsearch.app.GenericDocument) throws androidx.appsearch.exceptions.AppSearchException;
+ method public java.util.List<java.lang.Class<?>!> getNestedDocumentClasses() throws androidx.appsearch.exceptions.AppSearchException;
method public androidx.appsearch.app.AppSearchSchema getSchema() throws androidx.appsearch.exceptions.AppSearchException;
method public String getSchemaName();
method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
@@ -218,9 +224,12 @@
field public static final String JOIN_SPEC_AND_QUALIFIED_ID = "JOIN_SPEC_AND_QUALIFIED_ID";
field public static final String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
field public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH";
+ field public static final String SCHEMA_SET_DELETION_PROPAGATION = "SCHEMA_SET_DELETION_PROPAGATION";
field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
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_SUGGESTION = "SEARCH_SUGGESTION";
field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
field public static final String VERBATIM_SEARCH = "VERBATIM_SEARCH";
}
@@ -329,7 +338,6 @@
field public static final int AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL = 0; // 0x0
field public static final int AGGREGATION_SCORING_RESULT_COUNT = 1; // 0x1
field public static final int AGGREGATION_SCORING_SUM_RANKING_SIGNAL = 5; // 0x5
- field public static final String QUALIFIED_ID = "this.qualifiedId()";
}
public static final class JoinSpec.Builder {
@@ -495,6 +503,7 @@
method public boolean isVerbatimSearchEnabled();
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 = "*";
@@ -546,7 +555,7 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.VERBATIM_SEARCH) public androidx.appsearch.app.SearchSpec.Builder setVerbatimSearchEnabled(boolean);
}
- public class SearchSuggestionResult {
+ public final class SearchSuggestionResult {
method public String getSuggestedResult();
}
@@ -556,7 +565,7 @@
method public androidx.appsearch.app.SearchSuggestionResult.Builder setSuggestedResult(String);
}
- public class SearchSuggestionSpec {
+ 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.List<java.lang.String!> getFilterSchemas();
@@ -577,7 +586,7 @@
method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
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() throws androidx.appsearch.exceptions.AppSearchException;
+ method public androidx.appsearch.app.SearchSuggestionSpec build();
method public androidx.appsearch.app.SearchSuggestionSpec.Builder setRankingStrategy(int);
}
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index 7c0365c..124020c 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -40,6 +40,7 @@
implementation('androidx.core:core:1.7.0')
androidTestAnnotationProcessor project(':appsearch:appsearch-compiler')
+ androidTestImplementation project(':appsearch:appsearch-builtin-types')
androidTestImplementation project(':appsearch:appsearch-local-storage')
androidTestImplementation project(':appsearch:appsearch-platform-storage')
androidTestImplementation project(':appsearch:appsearch-test-util')
@@ -55,7 +56,7 @@
}
androidx {
- name = 'AndroidX AppSearch'
+ name = "AppSearch"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = '2019'
description = 'AndroidX AppSearch - App Indexing'
diff --git a/appsearch/appsearch/lint-baseline.xml b/appsearch/appsearch/lint-baseline.xml
index 00718c8..0f84e2d 100644
--- a/appsearch/appsearch/lint-baseline.xml
+++ b/appsearch/appsearch/lint-baseline.xml
@@ -13,15 +13,6 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1="public interface AppSearchObserverCallback extends ObserverCallback {}"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
errorLine1=" public @interface ResultCode {}"
errorLine2=" ~~~~~~~~~~">
<location
@@ -139,8 +130,8 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1=" public @DataType int getDataType() {"
- errorLine2=" ~~~~~~~~~~~">
+ errorLine1=" public int getDataType() {"
+ errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/androidx/appsearch/app/AppSearchSchema.java"/>
</issue>
@@ -211,6 +202,15 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
+ errorLine1="public @interface CanIgnoreReturnValue {}"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appsearch/annotation/CanIgnoreReturnValue.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
errorLine1="public final class DocumentClassFactoryRegistry {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -220,6 +220,24 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
+ errorLine1="public interface FeatureConstants {"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appsearch/app/FeatureConstants.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" String SEARCH_SUGGESTION = "SEARCH_SUGGESTION";"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appsearch/app/Features.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
errorLine1=" public GenericDocument(@NonNull Bundle bundle) {"
errorLine2=" ~~~~~~~~~~~~~~~">
<location
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 2e657eb..1f25e64 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
@@ -17,15 +17,26 @@
package androidx.appsearch.app;
import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES;
+import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID;
import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
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;
+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.annotation.Document;
+import androidx.appsearch.builtintypes.PotentialAction;
+import androidx.appsearch.builtintypes.Thing;
+import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.testutil.AppSearchEmail;
+import androidx.appsearch.util.DocumentIdUtil;
+import androidx.test.core.app.ApplicationProvider;
import com.google.auto.value.AutoValue;
import com.google.common.util.concurrent.ListenableFuture;
@@ -40,6 +51,8 @@
public abstract class AnnotationProcessorTestBase {
private AppSearchSession mSession;
+ private static final String TEST_PACKAGE_NAME =
+ ApplicationProvider.getApplicationContext().getPackageName();
private static final String DB_NAME_1 = "";
protected abstract ListenableFuture<AppSearchSession> createSearchSessionAsync(
@@ -241,7 +254,7 @@
assertThat(first.toArray()).isEqualTo(second.toArray());
}
- public static Gift createPopulatedGift() {
+ public static Gift createPopulatedGift() throws AppSearchException {
Gift gift = new Gift();
gift.mNamespace = "gift.namespace";
gift.mId = "gift.id";
@@ -295,6 +308,33 @@
}
}
+
+ @Document
+ static class CardAction {
+ @Document.Namespace
+ String mNamespace;
+
+ @Document.Id
+ String mId;
+ @Document.StringProperty(name = "cardRef",
+ joinableValueType = JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+ String mCardReference; // 3a
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof CardAction)) {
+ return false;
+ }
+ CardAction otherGift = (CardAction) other;
+ assertThat(otherGift.mNamespace).isEqualTo(this.mNamespace);
+ assertThat(otherGift.mId).isEqualTo(this.mId);
+ assertThat(otherGift.mCardReference).isEqualTo(this.mCardReference);
+ return true;
+ }
+ }
+
@Test
public void testAnnotationProcessor() throws Exception {
//TODO(b/156296904) add test for int, float, GenericDocument, and class with
@@ -323,9 +363,9 @@
@Test
public void testAnnotationProcessor_queryByType() throws Exception {
mSession.setSchemaAsync(
- new SetSchemaRequest.Builder()
- .addDocumentClasses(Card.class, Gift.class)
- .addSchemas(AppSearchEmail.SCHEMA).build())
+ new SetSchemaRequest.Builder()
+ .addDocumentClasses(Card.class, Gift.class)
+ .addSchemas(AppSearchEmail.SCHEMA).build())
.get();
// Create documents and index them
@@ -377,6 +417,61 @@
}
@Test
+ public void testAnnotationProcessor_simpleJoin() throws Exception {
+ assumeTrue(mSession.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+ mSession.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addDocumentClasses(Card.class, CardAction.class)
+ .build())
+ .get();
+
+ // Index a Card and a Gift referencing it.
+ Card peetsCard = new Card();
+ peetsCard.mNamespace = "personal";
+ peetsCard.mId = "peets1";
+ CardAction bdayGift = new CardAction();
+ bdayGift.mNamespace = "personal";
+ bdayGift.mId = "2023-jan-31";
+ bdayGift.mCardReference = DocumentIdUtil.createQualifiedId(TEST_PACKAGE_NAME, DB_NAME_1,
+ GenericDocument.fromDocumentClass(peetsCard));
+ checkIsBatchResultSuccess(mSession.putAsync(
+ new PutDocumentsRequest.Builder().addDocuments(peetsCard, bdayGift).build()));
+
+ // Retrieve cards with any given gifts.
+ SearchSpec innerSpec = new SearchSpec.Builder()
+ .addFilterDocumentClasses(CardAction.class)
+ .build();
+ JoinSpec js = new JoinSpec.Builder("cardRef")
+ .setNestedSearch(/*nestedQuery*/ "", innerSpec)
+ .build();
+ SearchResults resultsIter = mSession.search(/*queryExpression*/ "",
+ new SearchSpec.Builder()
+ .addFilterDocumentClasses(Card.class)
+ .setJoinSpec(js)
+ .build());
+
+ // Verify that search results include card(s) joined with gift(s).
+ List<SearchResult> results = resultsIter.getNextPageAsync().get();
+ assertThat(results).hasSize(1);
+ GenericDocument cardResultDoc = results.get(0).getGenericDocument();
+ assertThat(cardResultDoc.getId()).isEqualTo(peetsCard.mId);
+ List<SearchResult> joinedCardResults = results.get(0).getJoinedResults();
+ assertThat(joinedCardResults).hasSize(1);
+ GenericDocument giftResultDoc = joinedCardResults.get(0).getGenericDocument();
+ assertThat(giftResultDoc.getId()).isEqualTo(bdayGift.mId);
+ }
+
+ @Test
+ public void testAnnotationProcessor_onTAndBelow_joinNotSupported() throws Exception {
+ assumeFalse(mSession.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+ Exception e = assertThrows(UnsupportedOperationException.class,
+ () -> mSession.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addDocumentClasses(Card.class, CardAction.class)
+ .build()));
+ }
+
+ @Test
public void testGenericDocumentConversion() throws Exception {
Gift inGift = Gift.createPopulatedGift();
GenericDocument genericDocument1 = GenericDocument.fromDocumentClass(inGift);
@@ -472,4 +567,123 @@
List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
assertThat(documents).containsExactly(genericDocument);
}
+
+ @Test
+ public void testActionDocumentPutAndRetrieveHelper() throws Exception {
+ String namespace = "namespace";
+ String id = "docId";
+ String name = "View";
+ String uri = "package://view";
+ String description = "View action";
+ long creationMillis = 300;
+
+ GenericDocument genericDocAction = new GenericDocument.Builder<>(namespace, id,
+ "builtin:PotentialAction")
+ .setPropertyString("name", name)
+ .setPropertyString("uri", uri)
+ .setPropertyString("description", description)
+ .setCreationTimestampMillis(creationMillis)
+ .build();
+
+ mSession.setSchemaAsync(
+ new SetSchemaRequest.Builder().addDocumentClasses(PotentialAction.class)
+ .setForceOverride(true).build()).get();
+ checkIsBatchResultSuccess(
+ mSession.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(
+ genericDocAction).build()));
+
+ GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder(namespace)
+ .addIds(id)
+ .build();
+ List<GenericDocument> outDocuments = doGet(mSession, request);
+ assertThat(outDocuments).hasSize(1);
+ PotentialAction potentialAction =
+ outDocuments.get(0).toDocumentClass(PotentialAction.class);
+
+ assertThat(potentialAction.getName()).isEqualTo(name);
+ assertThat(potentialAction.getUri()).isEqualTo(uri);
+ assertThat(potentialAction.getDescription()).isEqualTo(description);
+ }
+
+ @Test
+ public void testDependentSchemas() throws Exception {
+ // Test that makes sure if you call setSchema on Thing, PotentialAction also goes in.
+ String namespace = "namespace";
+ String name = "View";
+ String uri = "package://view";
+ String description = "View action";
+ long creationMillis = 300;
+
+ GenericDocument genericDocAction = new GenericDocument.Builder<>(namespace, "actionid",
+ "builtin:PotentialAction")
+ .setPropertyString("name", name)
+ .setPropertyString("uri", uri)
+ .setPropertyString("description", description)
+ .setCreationTimestampMillis(creationMillis)
+ .build();
+
+ Thing thing = new Thing.Builder(namespace, "thingid")
+ .setName(name)
+ .setCreationTimestampMillis(creationMillis).build();
+
+ SetSchemaRequest request = new SetSchemaRequest.Builder().addDocumentClasses(Thing.class)
+ .setForceOverride(true).build();
+
+ // Both Thing and PotentialAction should be set as schemas
+ assertThat(request.getSchemas()).hasSize(2);
+ mSession.setSchemaAsync(request).get();
+
+ assertThat(mSession.getSchemaAsync().get().getSchemas()).hasSize(2);
+
+ // We should be able to put a PotentialAction as well as a Thing
+ checkIsBatchResultSuccess(
+ mSession.putAsync(new PutDocumentsRequest.Builder()
+ .addDocuments(thing)
+ .addGenericDocuments(genericDocAction)
+ .build()));
+
+ GetByDocumentIdRequest getDocRequest = new GetByDocumentIdRequest.Builder(namespace)
+ .addIds("thingid")
+ .build();
+ List<GenericDocument> outDocuments = doGet(mSession, getDocRequest);
+ assertThat(outDocuments).hasSize(1);
+ Thing potentialAction = outDocuments.get(0).toDocumentClass(Thing.class);
+
+ assertThat(potentialAction.getNamespace()).isEqualTo(namespace);
+ assertThat(potentialAction.getId()).isEqualTo("thingid");
+ assertThat(potentialAction.getName()).isEqualTo(name);
+ assertThat(potentialAction.getPotentialActions()).isEmpty();
+ }
+
+ @Document
+ static class Outer {
+ @Document.Id String mId;
+ @Document.Namespace String mNamespace;
+ @Document.DocumentProperty Middle mMiddle;
+ }
+
+ @Document
+ static class Middle {
+ @Document.Id String mId;
+ @Document.Namespace String mNamespace;
+ @Document.DocumentProperty Inner mInner;
+ }
+
+ @Document
+ static class Inner {
+ @Document.Id String mId;
+ @Document.Namespace String mNamespace;
+ @Document.StringProperty String mContents;
+ }
+
+ @Test
+ public void testMultipleDependentSchemas() throws Exception {
+ SetSchemaRequest request = new SetSchemaRequest.Builder().addDocumentClasses(Outer.class)
+ .setForceOverride(true).build();
+
+ // Outer, as well as Middle and Inner should be set.
+ assertThat(request.getSchemas()).hasSize(3);
+ mSession.setSchemaAsync(request).get();
+ assertThat(mSession.getSchemaAsync().get().getSchemas()).hasSize(3);
+ }
}
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 8310664..c289e9e 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
@@ -17,12 +17,18 @@
package androidx.appsearch.app;
import static androidx.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
+import static androidx.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments;
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 com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
@@ -32,6 +38,7 @@
import org.junit.Test;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.ExecutorService;
public abstract class AppSearchSessionInternalTestBase {
@@ -167,4 +174,382 @@
.build()).get();
assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour);
}
+
+ // TODO(b/258715421): move this test to cts test once we un-hide schema type grouping API.
+ @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("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+ AppSearchEmail inEmail2 =
+ new AppSearchEmail.Builder("namespace1", "id2")
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+ AppSearchEmail inEmail3 =
+ new AppSearchEmail.Builder("namespace2", "id3")
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
+ AppSearchEmail inEmail4 =
+ new AppSearchEmail.Builder("namespace2", "id4")
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
+ AppSearchEmail inEmail5 =
+ new AppSearchEmail.Builder("namespace2", "id5")
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(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/258715421): move this test to cts test once we un-hide schema type grouping API.
+ @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("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+ AppSearchEmail inEmail2 =
+ new AppSearchEmail.Builder("namespace1", "id2")
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+ AppSearchEmail inEmail3 =
+ new AppSearchEmail.Builder("namespace2", "id3")
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
+ AppSearchEmail inEmail4 =
+ new AppSearchEmail.Builder("namespace2", "id4")
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(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.");
+ }
+
+ // 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(new StringPropertyConfig.Builder("normalStr")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("optionalQualifiedIdStr")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+ .build()
+ ).addProperty(new StringPropertyConfig.Builder("requiredQualifiedIdStr")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+ .setDeletionPropagation(true)
+ .build()
+ ).build();
+
+ SetSchemaRequest request = new SetSchemaRequest.Builder()
+ .addSchemas(inSchema).build();
+
+ mDb1.setSchemaAsync(request).get();
+
+ Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
+ assertThat(actual).hasSize(1);
+ 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.");
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSuggestionSpecInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSuggestionSpecInternalTest.java
index 3130c92..bc68f37 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSuggestionSpecInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSuggestionSpecInternalTest.java
@@ -16,14 +16,10 @@
package androidx.appsearch.app;
-import static androidx.appsearch.app.AppSearchResult.RESULT_INVALID_ARGUMENT;
-
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
-import androidx.appsearch.exceptions.AppSearchException;
-
import com.google.common.collect.ImmutableList;
import org.junit.Test;
@@ -54,12 +50,11 @@
@Test
public void testPropertyFilterMustMatchSchemaFilter() throws Exception {
- AppSearchException e = assertThrows(AppSearchException.class,
+ IllegalStateException e = assertThrows(IllegalStateException.class,
() -> new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
.addFilterSchemas("Person")
.addFilterProperties("Email", ImmutableList.of("Subject", "body"))
.build());
- assertThat(e.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
assertThat(e).hasMessageThat().contains("The schema: Email exists in the "
+ "property filter but doesn't exist in the schema filter.");
}
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 799584a..37e1255 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,15 @@
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;
+
import org.junit.Test;
+import java.util.List;
+
/** Tests for private APIs of {@link SetSchemaResponse}. */
public class SetSchemaResponseInternalTest {
@Test
@@ -67,4 +74,41 @@
assertThat(rebuild.getMigratedTypes()).containsExactly("migrated1", "migrated2");
assertThat(rebuild.getMigrationFailures()).containsExactly(failure1, failure2);
}
+
+ // TODO(b/268521214): Move test to cts once deletion propagation is available in framework.
+ @Test
+ public void testPropertyConfig_deletionPropagation() {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Test")
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("qualifiedId1")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+ .setDeletionPropagation(true)
+ .build())
+ .build();
+
+ assertThat(schema.getSchemaType()).isEqualTo("Test");
+ List<PropertyConfig> properties = schema.getProperties();
+ assertThat(properties).hasSize(1);
+
+ assertThat(properties.get(0).getName()).isEqualTo("qualifiedId1");
+ assertThat(properties.get(0).getCardinality())
+ .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/AppSearchSchemaMigrationLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java
index 75b3888..0d0ac6e 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java
@@ -22,12 +22,10 @@
import androidx.appsearch.app.AppSearchSession;
import androidx.appsearch.localstorage.LocalStorage;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.FlakyTest;
import com.google.common.util.concurrent.ListenableFuture;
-@FlakyTest(bugId = 242761389)
-public class AppSearchSchemaMigrationLocalCtsTest extends AppSearchSchemaMigrationCtsTestBase{
+public class AppSearchSchemaMigrationLocalCtsTest extends AppSearchSchemaMigrationCtsTestBase {
@Override
protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
Context context = ApplicationProvider.getApplicationContext();
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 bf470fd..57cba83 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
@@ -147,20 +147,22 @@
AppSearchSchema emailSchema1 = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
.build();
- Throwable throwable = assertThrows(ExecutionException.class,
- () -> mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
- .addSchemas(emailSchema1).build()).get()).getCause();
- assertThat(throwable).isInstanceOf(AppSearchException.class);
- AppSearchException exception = (AppSearchException) throwable;
+ SetSchemaRequest setSchemaRequest1 =
+ new SetSchemaRequest.Builder().addSchemas(emailSchema1).build();
+ ExecutionException executionException =
+ assertThrows(ExecutionException.class,
+ () -> mDb1.setSchemaAsync(setSchemaRequest1).get());
+ assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+ AppSearchException exception = (AppSearchException) executionException.getCause();
assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA);
assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
assertThat(exception).hasMessageThat().contains("Incompatible types: {builtin:Email}");
- throwable = assertThrows(ExecutionException.class,
- () -> mDb1.setSchemaAsync(new SetSchemaRequest.Builder().build()).get()).getCause();
-
- assertThat(throwable).isInstanceOf(AppSearchException.class);
- exception = (AppSearchException) throwable;
+ SetSchemaRequest setSchemaRequest2 = new SetSchemaRequest.Builder().build();
+ executionException = assertThrows(ExecutionException.class,
+ () -> mDb1.setSchemaAsync(setSchemaRequest2).get());
+ assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+ exception = (AppSearchException) executionException.getCause();
assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA);
assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
assertThat(exception).hasMessageThat().contains("Deleted types: {builtin:Email}");
@@ -515,7 +517,6 @@
@Test
public void testGetSchema_longPropertyIndexingTypeNone_succeeds() throws Exception {
- assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH));
AppSearchSchema inSchema = new AppSearchSchema.Builder("Test")
.addProperty(new LongPropertyConfig.Builder("long")
.setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
@@ -546,8 +547,10 @@
SetSchemaRequest request = new SetSchemaRequest.Builder()
.addSchemas(inSchema).build();
- assertThrows(UnsupportedOperationException.class, () ->
+ UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class, () ->
mDb1.setSchemaAsync(request).get());
+ assertThat(e.getMessage()).isEqualTo("LongProperty.INDEXING_TYPE_RANGE is not "
+ + "supported on this AppSearch implementation.");
}
@Test
@@ -579,7 +582,6 @@
@Test
public void testGetSchema_joinableValueTypeNone_succeeds() throws Exception {
- assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
AppSearchSchema inSchema = new AppSearchSchema.Builder("Test")
.addProperty(new StringPropertyConfig.Builder("optionalString")
.setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
@@ -618,8 +620,11 @@
SetSchemaRequest request = new SetSchemaRequest.Builder()
.addSchemas(inSchema).build();
- assertThrows(UnsupportedOperationException.class, () ->
+ UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class, () ->
mDb1.setSchemaAsync(request).get());
+ assertThat(e.getMessage()).isEqualTo(
+ "StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID is not supported on this "
+ + "AppSearch implementation.");
}
@Test
@@ -947,10 +952,11 @@
assertThat(outEmail).isEqualTo(email);
// Try to remove the email schema. This should fail as it's an incompatible change.
- Throwable failResult1 = assertThrows(
- ExecutionException.class,
- () -> mDb1.setSchemaAsync(new SetSchemaRequest.Builder().build()).get()).getCause();
- assertThat(failResult1).isInstanceOf(AppSearchException.class);
+ SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder().build();
+ ExecutionException executionException = assertThrows(ExecutionException.class,
+ () -> mDb1.setSchemaAsync(setSchemaRequest).get());
+ assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+ AppSearchException failResult1 = (AppSearchException) executionException.getCause();
assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
assertThat(failResult1).hasMessageThat().contains(
"Deleted types: {builtin:Email}");
@@ -1020,10 +1026,11 @@
// Try to remove the email schema in database1. This should fail as it's an incompatible
// change.
- Throwable failResult1 = assertThrows(
- ExecutionException.class,
- () -> mDb1.setSchemaAsync(new SetSchemaRequest.Builder().build()).get()).getCause();
- assertThat(failResult1).isInstanceOf(AppSearchException.class);
+ SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder().build();
+ ExecutionException executionException = assertThrows(ExecutionException.class,
+ () -> mDb1.setSchemaAsync(setSchemaRequest).get());
+ assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+ AppSearchException failResult1 = (AppSearchException) executionException.getCause();
assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
assertThat(failResult1).hasMessageThat().contains(
"Deleted types: {builtin:Email}");
@@ -1619,21 +1626,17 @@
checkIsBatchResultSuccess(mDb1.putAsync(
new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
- // TODO(b/208654892); Remove setListFilterQueryLanguageEnabled once advanced query is fully
- // supported.
// Query for the document
// Use advanced query but disable NUMERIC_SEARCH in the SearchSpec.
SearchResults searchResults = mDb1.search("price < 20",
new SearchSpec.Builder()
- .setListFilterQueryLanguageEnabled(true)
.setNumericSearchEnabled(false)
.build());
- Throwable failResult = assertThrows(
- ExecutionException.class,
- () -> searchResults.getNextPageAsync().get()).getCause();
- assertThat(failResult).isInstanceOf(AppSearchException.class);
- AppSearchException exception = (AppSearchException) failResult;
+ 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.NUMERIC_SEARCH);
@@ -1738,6 +1741,130 @@
}
@Test
+ public void testQuery_advancedRankingWithPropertyWeights() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
+
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(AppSearchEmail.SCHEMA)
+ .build()).get();
+
+ // Index a document
+ AppSearchEmail inEmail =
+ new AppSearchEmail.Builder("namespace", "id1")
+ .setFrom("test from")
+ .setTo("test to")
+ .setSubject("subject")
+ .setBody("test body")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+ // Query for the document, and set an advanced ranking expression that evaluates to 0.7.
+ SearchResults searchResults = mDb1.search("test", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE,
+ ImmutableMap.of("from", 0.1, "to", 0.2,
+ "subject", 2.0, "body", 0.4))
+ // this.propertyWeights() returns normalized property weights, in which each
+ // weight is divided by the maximum weight.
+ // As a result, this expression will evaluates to the list {0.1 / 2.0, 0.2 / 2.0,
+ // 0.4 / 2.0}, since the matched properties are "from", "to" and "body", and the
+ // maximum weight provided is 2.0.
+ // Thus, sum(this.propertyWeights()) will be evaluated to 0.05 + 0.1 + 0.2 = 0.35.
+ .setRankingStrategy("sum(this.propertyWeights())")
+ .build());
+ List<SearchResult> results = retrieveAllSearchResults(searchResults);
+ assertThat(results).hasSize(1);
+ assertThat(results.get(0).getGenericDocument()).isEqualTo(inEmail);
+ assertThat(results.get(0).getRankingSignal()).isEqualTo(0.35);
+ }
+
+ @Test
+ public void testQuery_advancedRankingWithJoin() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
+ assumeTrue(mDb1.getFeatures()
+ .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+ // A full example of how join might be used
+ 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()
+ ).build();
+
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
+ .build()).get();
+
+ // Index a document
+ AppSearchEmail inEmail =
+ new AppSearchEmail.Builder("namespace", "id1")
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .setScore(1)
+ .build();
+
+ String qualifiedId = DocumentIdUtil.createQualifiedId(mContext.getPackageName(), DB_NAME_1,
+ "namespace", "id1");
+ GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id2", "ViewAction")
+ .setScore(1)
+ .setPropertyString("entityId", qualifiedId)
+ .setPropertyString("note", "Viewed email on Monday").build();
+ GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
+ .setScore(2)
+ .setPropertyString("entityId", qualifiedId)
+ .setPropertyString("note", "Viewed email on Tuesday").build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, viewAction1,
+ viewAction2).build()));
+
+ SearchSpec nestedSearchSpec =
+ new SearchSpec.Builder()
+ .setRankingStrategy("2 * this.documentScore()")
+ .setOrder(SearchSpec.ORDER_ASCENDING)
+ .build();
+
+ JoinSpec js = new JoinSpec.Builder("entityId")
+ .setNestedSearch("", nestedSearchSpec)
+ .build();
+
+ SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
+ // this.childrenScores() evaluates to the list {1 * 2, 2 * 2}.
+ // Thus, sum(this.childrenScores()) evaluates to 6.
+ .setRankingStrategy("sum(this.childrenScores())")
+ .setJoinSpec(js)
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .build());
+
+ List<SearchResult> sr = searchResults.getNextPageAsync().get();
+
+ assertThat(sr).hasSize(1);
+ assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id1");
+ assertThat(sr.get(0).getJoinedResults()).hasSize(2);
+ assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);
+ assertThat(sr.get(0).getJoinedResults().get(1).getGenericDocument()).isEqualTo(viewAction2);
+ assertThat(sr.get(0).getRankingSignal()).isEqualTo(6.0);
+ }
+
+ @Test
public void testQuery_invalidAdvancedRanking() throws Exception {
assumeTrue(mDb1.getFeatures().isFeatureSupported(
Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
@@ -1764,16 +1891,54 @@
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.setRankingStrategy("sqrt()")
.build());
- Throwable throwable = assertThrows(ExecutionException.class,
- () -> searchResults.getNextPageAsync().get()).getCause();
- assertThat(throwable).isInstanceOf(AppSearchException.class);
- AppSearchException exception = (AppSearchException) throwable;
+ 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(
"Math functions must have at least one argument.");
}
@Test
+ public void testQuery_invalidAdvancedRankingWithChildrenScores() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(
+ Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
+ assumeTrue(mDb1.getFeatures()
+ .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(AppSearchEmail.SCHEMA)
+ .build()).get();
+
+ // Index a document
+ AppSearchEmail inEmail =
+ new AppSearchEmail.Builder("namespace", "id1")
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+ SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ // Using this.childrenScores() without the context of a join is invalid.
+ .setRankingStrategy("sum(this.childrenScores())")
+ .build());
+ 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(
+ "childrenScores must only be used with join");
+ }
+
+ @Test
public void testQuery_unsupportedAdvancedRanking() throws Exception {
// Assume that advanced ranking has not been supported.
assumeFalse(mDb1.getFeatures().isFeatureSupported(
@@ -2972,8 +3137,7 @@
assertThat(doGet(mDb1, "namespace", "id1", "id2", "id3")).hasSize(3);
// Delete the email type
- mDb1.removeAsync("",
- new SearchSpec.Builder()
+ mDb1.removeAsync("", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
.addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
.build())
@@ -3024,8 +3188,7 @@
assertThat(doGet(mDb2, "namespace", "id2")).hasSize(1);
// Delete the email type in instance 1
- mDb1.removeAsync("",
- new SearchSpec.Builder()
+ mDb1.removeAsync("", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
.addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
.build())
@@ -3086,8 +3249,7 @@
assertThat(doGet(mDb1, /*namespace=*/"document", "id3")).hasSize(1);
// Delete the email namespace
- mDb1.removeAsync("",
- new SearchSpec.Builder()
+ mDb1.removeAsync("", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
.addFilterNamespaces("email")
.build())
@@ -3142,8 +3304,7 @@
assertThat(doGet(mDb2, /*namespace=*/"email", "id2")).hasSize(1);
// Delete the email namespace in instance 1
- mDb1.removeAsync("",
- new SearchSpec.Builder()
+ mDb1.removeAsync("", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
.addFilterNamespaces("email")
.build())
@@ -3198,8 +3359,7 @@
assertThat(doGet(mDb2, "namespace", "id2")).hasSize(1);
// Delete the all document in instance 1
- mDb1.removeAsync("",
- new SearchSpec.Builder()
+ mDb1.removeAsync("", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
.build())
.get();
@@ -3273,8 +3433,7 @@
assertThat(documents).hasSize(2);
// Delete the all document in instance 1 with TERM_MATCH_PREFIX
- mDb1.removeAsync("",
- new SearchSpec.Builder()
+ mDb1.removeAsync("", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
.build())
.get();
@@ -3285,8 +3444,7 @@
assertThat(documents).isEmpty();
// Delete the all document in instance 2 with TERM_MATCH_EXACT_ONLY
- mDb2.removeAsync("",
- new SearchSpec.Builder()
+ mDb2.removeAsync("", new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.build())
.get();
@@ -3331,7 +3489,7 @@
// Delete the all documents
mDb1.removeAsync("", new SearchSpec.Builder()
- .setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get();
+ .setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get();
// Make sure it's still gone
getResult = mDb1.getByDocumentIdAsync(
@@ -3346,8 +3504,7 @@
assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
- () -> mDb2.removeAsync("",
- new SearchSpec.Builder()
+ () -> mDb2.removeAsync("", new SearchSpec.Builder()
.setJoinSpec(new JoinSpec.Builder("entityId").build())
.build()));
assertThat(e.getMessage()).isEqualTo("JoinSpec not allowed in removeByQuery, "
@@ -3480,12 +3637,12 @@
mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1").build()).get();
// Use an incorrect namespace; it fails
- ExecutionException e = assertThrows(
- ExecutionException.class,
- () -> mDb1.reportUsageAsync(
- new ReportUsageRequest.Builder("namespace2", "id1").build()).get());
- assertThat(e).hasCauseThat().isInstanceOf(AppSearchException.class);
- AppSearchException cause = (AppSearchException) e.getCause();
+ ReportUsageRequest reportUsageRequest =
+ new ReportUsageRequest.Builder("namespace2", "id1").build();
+ ExecutionException executionException = assertThrows(ExecutionException.class,
+ () -> mDb1.reportUsageAsync(reportUsageRequest).get());
+ assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+ AppSearchException cause = (AppSearchException) executionException.getCause();
assertThat(cause.getResultCode()).isEqualTo(RESULT_NOT_FOUND);
}
@@ -3596,8 +3753,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, /*resultLimit=*/ 1)
.build());
documents = convertSearchResultsToDocuments(searchResults);
assertThat(documents).containsExactly(inEmail4, inEmail2);
@@ -3606,9 +3762,8 @@
// namespace should be returned ('email4' 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)
+ .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+ | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
.build());
documents = convertSearchResultsToDocuments(searchResults);
assertThat(documents).containsExactly(inEmail4, inEmail2);
@@ -3727,10 +3882,10 @@
final SetSchemaRequest newRequest =
new SetSchemaRequest.Builder().addSchemas(newNestedSchema,
newSchema).build();
- Throwable throwable = assertThrows(ExecutionException.class,
- () -> mDb1.setSchemaAsync(newRequest).get()).getCause();
- assertThat(throwable).isInstanceOf(AppSearchException.class);
- AppSearchException exception = (AppSearchException) throwable;
+ ExecutionException executionException = assertThrows(ExecutionException.class,
+ () -> mDb1.setSchemaAsync(newRequest).get());
+ assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+ AppSearchException exception = (AppSearchException) executionException.getCause();
assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA);
assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
assertThat(exception).hasMessageThat().contains("Incompatible types: {TypeA}");
@@ -3840,6 +3995,25 @@
}
@Test
+ public void testRfc822_unsupportedFeature_throwsException() {
+ assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.TOKENIZER_TYPE_RFC822));
+
+ AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+ .addProperty(new StringPropertyConfig.Builder("address")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_RFC822)
+ .build()
+ ).build();
+
+ Exception e = assertThrows(IllegalArgumentException.class, () ->
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+ .setForceOverride(true).addSchemas(emailSchema).build()).get());
+ assertThat(e.getMessage()).isEqualTo("tokenizerType is out of range of [0, 1] (too high)");
+ }
+
+
+ @Test
public void testQuery_verbatimSearch() throws Exception {
assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.VERBATIM_SEARCH));
AppSearchSchema verbatimSchema = new AppSearchSchema.Builder("VerbatimSchema")
@@ -3886,26 +4060,178 @@
.build();
mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
- // TODO(b/208654892) Disable ListFilterQueryLanguage once EXPERIMENTAL_ICING_ADVANCED_QUERY
- // is fully supported.
// 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()
- .setListFilterQueryLanguageEnabled(true)
.setVerbatimSearchEnabled(false)
.build());
- Throwable throwable = assertThrows(ExecutionException.class,
- () -> searchResults.getNextPageAsync().get()).getCause();
- assertThat(throwable).isInstanceOf(AppSearchException.class);
- AppSearchException exception = (AppSearchException) throwable;
+ 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.VERBATIM_SEARCH);
}
@Test
+ public void testQuery_listFilterQueryWithEnablingFeatureSucceeds() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+ 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 email = new GenericDocument.Builder<>(
+ "namespace1", "id1", "Schema")
+ .setPropertyString("prop", "Hello, world!")
+ .build();
+ mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
+
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setListFilterQueryLanguageEnabled(true)
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .build();
+ // Support for function calls `search`, `createList` was added in list filters
+ SearchResults searchResults = mDb1.search("search(\"hello\", createList(\"prop\"))",
+ searchSpec);
+ List<SearchResult> page = searchResults.getNextPageAsync().get();
+ assertThat(page).hasSize(1);
+ assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
+
+ // Support for prefix operator * was added in list filters.
+ searchResults = mDb1.search("wor*", searchSpec);
+ page = searchResults.getNextPageAsync().get();
+ assertThat(page).hasSize(1);
+ assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
+
+ // Combining negations with compound statements and property restricts was added in list
+ // filters.
+ searchResults = mDb1.search("NOT (foo OR otherProp:hello)", searchSpec);
+ page = searchResults.getNextPageAsync().get();
+ assertThat(page).hasSize(1);
+ assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
+ }
+
+ @Test
+ public void testQuery_listFilterQueryWithoutEnablingFeatureFails() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+ 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 email = new GenericDocument.Builder<>(
+ "namespace1", "id1", "Schema")
+ .setPropertyString("prop", "Hello, world!")
+ .build();
+ mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
+
+ // Disable LIST_FILTER_QUERY_LANGUAGE in the SearchSpec.
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setListFilterQueryLanguageEnabled(false)
+ .build();
+ SearchResults searchResults = mDb1.search("search(\"hello\", createList(\"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);
+
+ SearchResults searchResults2 = mDb1.search("wor*", 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(Features.LIST_FILTER_QUERY_LANGUAGE);
+
+ SearchResults searchResults3 = mDb1.search("NOT (foo OR otherProp:hello)", searchSpec);
+ executionException = assertThrows(ExecutionException.class,
+ () -> searchResults3.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(Features.LIST_FILTER_QUERY_LANGUAGE);
+ }
+
+ @Test
+ public void testQuery_listFilterQueryFeatures_notSupported() throws Exception {
+ assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH));
+ assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.VERBATIM_SEARCH));
+ assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+
+ // UnsupportedOperationException will be thrown with these queries so no need to
+ // define a schema and index document.
+ SearchSpec.Builder builder = new SearchSpec.Builder();
+ SearchSpec searchSpec1 = builder.setNumericSearchEnabled(true).build();
+ SearchSpec searchSpec2 = builder.setVerbatimSearchEnabled(true).build();
+ SearchSpec searchSpec3 = builder.setListFilterQueryLanguageEnabled(true).build();
+
+ assertThrows(UnsupportedOperationException.class, () ->
+ mDb1.search("\"Hello, world!\"", searchSpec1));
+ assertThrows(UnsupportedOperationException.class, () ->
+ mDb1.search("\"Hello, world!\"", searchSpec2));
+ assertThrows(UnsupportedOperationException.class, () ->
+ mDb1.search("\"Hello, world!\"", searchSpec3));
+ }
+
+ @Test
+ public void testQuery_propertyWeightsNotSupported() throws Exception {
+ assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
+
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder()
+ .addSchemas(AppSearchEmail.SCHEMA)
+ .build()).get();
+
+ // Index two documents
+ AppSearchEmail email1 =
+ new AppSearchEmail.Builder("namespace", "id1")
+ .setCreationTimestampMillis(1000)
+ .setSubject("foo")
+ .build();
+ AppSearchEmail email2 =
+ new AppSearchEmail.Builder("namespace", "id2")
+ .setCreationTimestampMillis(1000)
+ .setBody("foo")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder()
+ .addGenericDocuments(email1, email2).build()));
+
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+ .setOrder(SearchSpec.ORDER_DESCENDING)
+ .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of("subject",
+ 2.0, "body", 0.5))
+ .build();
+ UnsupportedOperationException exception =
+ assertThrows(UnsupportedOperationException.class,
+ () -> mDb1.search("Hello", searchSpec));
+ assertThat(exception).hasMessageThat().contains("Property weights are not supported");
+ }
+
+ @Test
public void testQuery_propertyWeights() throws Exception {
assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
@@ -4159,7 +4485,7 @@
.isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
// A full example of how join might be used
- AppSearchSchema actionSchema = new AppSearchSchema.Builder("BookmarkAction")
+ AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
.addProperty(new StringPropertyConfig.Builder("entityId")
.setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
@@ -4203,18 +4529,29 @@
String qualifiedId = DocumentIdUtil.createQualifiedId(mContext.getPackageName(), DB_NAME_1,
"namespace", "id1");
- GenericDocument join = new GenericDocument.Builder<>("NS", "id3", "BookmarkAction")
+ GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
+ .setScore(1)
.setPropertyString("entityId", qualifiedId)
- .setPropertyString("note", "Hi this is a joined doc").build();
+ .setPropertyString("note", "Viewed email on Monday").build();
+ GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
+ .setScore(2)
+ .setPropertyString("entityId", qualifiedId)
+ .setPropertyString("note", "Viewed email on Tuesday").build();
checkIsBatchResultSuccess(mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2, join)
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
+ viewAction1, viewAction2)
.build()));
- SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
+ SearchSpec nestedSearchSpec =
+ new SearchSpec.Builder()
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
+ .setOrder(SearchSpec.ORDER_ASCENDING)
+ .build();
JoinSpec js = new JoinSpec.Builder("entityId")
.setNestedSearch("", nestedSearchSpec)
.setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+ .setMaxJoinedResultCount(1)
.build();
SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
@@ -4230,7 +4567,7 @@
assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id1");
assertThat(sr.get(0).getJoinedResults()).hasSize(1);
- assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(join);
+ assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);
assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id2");
@@ -4239,26 +4576,35 @@
}
@Test
- public void testJoinWithoutSupport() throws Exception {
+ public void testJoin_unsupportedFeature_throwsException() throws Exception {
assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
JoinSpec js = new JoinSpec.Builder("entityId").setNestedSearch("", nestedSearchSpec)
.build();
- SearchResults searchResults = mDb1.search("", new SearchSpec.Builder()
- .setJoinSpec(js)
- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
- .build());
+ Exception e = assertThrows(UnsupportedOperationException.class, () -> mDb1.search(
+ /*queryExpression */ "",
+ new SearchSpec.Builder()
+ .setJoinSpec(js)
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .build()));
+ assertThat(e.getMessage()).isEqualTo("JoinSpec is not available on this AppSearch "
+ + "implementation.");
+ }
- Exception e = assertThrows(UnsupportedOperationException.class, () ->
- searchResults.getNextPageAsync().get());
- assertThat(e).isInstanceOf(UnsupportedOperationException.class);
- assertThat(e.getMessage()).isEqualTo("Searching with a SearchSpec containing a JoinSpec "
- + "is not supported on this AppSearch implementation.");
+ @Test
+ public void testSearchSuggestion_notSupported() throws Exception {
+ assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
+
+ assertThrows(UnsupportedOperationException.class, () ->
+ mDb1.searchSuggestionAsync(
+ /*suggestionQueryExpression=*/"t",
+ new SearchSuggestionSpec.Builder(/*totalResultCount=*/2).build()).get());
}
@Test
public void testSearchSuggestion() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
// Schema registration
AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
new StringPropertyConfig.Builder("body")
@@ -4311,6 +4657,7 @@
@Test
public void testSearchSuggestion_namespaceFilter() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
// Schema registration
AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
new StringPropertyConfig.Builder("body")
@@ -4374,6 +4721,7 @@
@Test
public void testSearchSuggestion_documentIdFilter() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
// Schema registration
AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
new StringPropertyConfig.Builder("body")
@@ -4449,6 +4797,7 @@
@Test
public void testSearchSuggestion_schemaFilter() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
// Schema registration
AppSearchSchema schemaType1 = new AppSearchSchema.Builder("Type1").addProperty(
new StringPropertyConfig.Builder("body")
@@ -4527,6 +4876,7 @@
@Test
public void testSearchSuggestion_differentPrefix() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
// Schema registration
AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
new StringPropertyConfig.Builder("body")
@@ -4579,6 +4929,7 @@
@Test
public void testSearchSuggestion_differentRankingStrategy() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
// Schema registration
AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
new StringPropertyConfig.Builder("body")
@@ -4645,6 +4996,7 @@
@Test
public void testSearchSuggestion_removeDocument() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
// Schema registration
AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
new StringPropertyConfig.Builder("body")
@@ -4697,6 +5049,7 @@
@Test
public void testSearchSuggestion_replacementDocument() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
// Schema registration
AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
new StringPropertyConfig.Builder("body")
@@ -4747,37 +5100,8 @@
}
@Test
- public void testSearchSuggestion_ignoreOperators() throws Exception {
- // Schema registration
- AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
- new StringPropertyConfig.Builder("body")
- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
- .build())
- .build();
- mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
-
- // Index documents
- GenericDocument doc = new GenericDocument.Builder<>("namespace", "id", "Type")
- .setPropertyString("body", "two original")
- .build();
-
- checkIsBatchResultSuccess(mDb1.putAsync(
- new PutDocumentsRequest.Builder().addGenericDocuments(doc)
- .build()));
-
- SearchSuggestionResult resultTwoOriginal =
- new SearchSuggestionResult.Builder().setSuggestedResult("two original").build();
-
- List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
- /*suggestionQueryExpression=*/"two OR",
- new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
- assertThat(suggestions).containsExactly(resultTwoOriginal);
- }
-
- @Test
public void testSearchSuggestion_twoInstances() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
// Schema registration
AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
new StringPropertyConfig.Builder("body")
@@ -4817,4 +5141,120 @@
new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
assertThat(suggestions).isEmpty();
}
+
+ @Test
+ public void testSearchSuggestion_multipleTerms() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
+ // Schema registration
+ AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
+ new StringPropertyConfig.Builder("body")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .build();
+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+ // Index documents
+ GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type")
+ .setPropertyString("body", "bar fo")
+ .build();
+ GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type")
+ .setPropertyString("body", "cat foo")
+ .build();
+ GenericDocument doc3 = new GenericDocument.Builder<>("namespace", "id3", "Type")
+ .setPropertyString("body", "fool")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3)
+ .build()));
+
+ // 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();
+ SearchSuggestionResult barFo =
+ new SearchSuggestionResult.Builder().setSuggestedResult("bar fo").build();
+ assertThat(suggestions).containsExactly(barFo);
+
+ // Search for "(bar OR cat) AND f" both document1 "bar fo" and document2 "cat foo" could
+ // match.
+ suggestions = mDb1.searchSuggestionAsync(
+ /*suggestionQueryExpression=*/"bar OR cat f",
+ new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+ SearchSuggestionResult barCatFo =
+ new SearchSuggestionResult.Builder().setSuggestedResult("bar OR cat fo").build();
+ SearchSuggestionResult barCatFoo =
+ new SearchSuggestionResult.Builder().setSuggestedResult("bar OR cat foo").build();
+ assertThat(suggestions).containsExactly(barCatFo, barCatFoo);
+
+ // 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();
+ SearchSuggestionResult barCatOrFo =
+ new SearchSuggestionResult.Builder().setSuggestedResult("(bar cat) OR fo").build();
+ SearchSuggestionResult barCatOrFoo =
+ new SearchSuggestionResult.Builder().setSuggestedResult("(bar cat) OR foo").build();
+ SearchSuggestionResult barCatOrFool =
+ new SearchSuggestionResult.Builder()
+ .setSuggestedResult("(bar cat) OR fool").build();
+ assertThat(suggestions).containsExactly(barCatOrFo, barCatOrFoo, barCatOrFool);
+
+ // 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();
+ SearchSuggestionResult noBarFoo =
+ new SearchSuggestionResult.Builder().setSuggestedResult("-bar foo").build();
+ SearchSuggestionResult noBarFool =
+ new SearchSuggestionResult.Builder().setSuggestedResult("-bar fool").build();
+ assertThat(suggestions).containsExactly(noBarFoo, noBarFool);
+ }
+
+ @Test
+ public void testSearchSuggestion_PropertyRestriction() throws Exception {
+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
+ // Schema registration
+ AppSearchSchema schema = new AppSearchSchema.Builder("Type")
+ .addProperty(new StringPropertyConfig.Builder("subject")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .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 doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type")
+ .setPropertyString("subject", "bar fo")
+ .setPropertyString("body", "fool")
+ .build();
+ GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type")
+ .setPropertyString("subject", "bar cat foo")
+ .setPropertyString("body", "fool")
+ .build();
+ GenericDocument doc3 = new GenericDocument.Builder<>("namespace", "ide", "Type")
+ .setPropertyString("subject", "fool")
+ .setPropertyString("body", "fool")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3)
+ .build()));
+
+ // Search for "bar AND subject:f"
+ List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
+ /*suggestionQueryExpression=*/"bar subject:f",
+ new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+ SearchSuggestionResult barSubjectFo =
+ new SearchSuggestionResult.Builder().setSuggestedResult("bar subject:fo").build();
+ SearchSuggestionResult barSubjectFoo =
+ new SearchSuggestionResult.Builder().setSuggestedResult("bar subject:foo").build();
+ assertThat(suggestions).containsExactly(barSubjectFo, barSubjectFoo);
+ }
}
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 56bf168..ac08790 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
@@ -160,63 +160,5 @@
// b/229770338 was fixed in Android T, this test will fail on S_V2 devices and below.
assumeTrue(BuildCompat.isAtLeastT());
super.testEmojiSnippet();
- }@Override
- @Test
- public void testSearchSuggestion() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
-
- @Override
- @Test
- public void testSearchSuggestion_namespaceFilter() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
-
- @Override
- @Test
- public void testSearchSuggestion_documentIdFilter() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
-
- @Override
- @Test
- public void testSearchSuggestion_differentPrefix() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
-
- @Override
- @Test
- public void testSearchSuggestion_differentRankingStrategy() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
-
- @Override
- @Test
- public void testSearchSuggestion_removeDocument() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
-
- @Override
- @Test
- public void testSearchSuggestion_replacementDocument() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
-
- @Override
- @Test
- public void testSearchSuggestion_ignoreOperators() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
-
- @Override
- @Test
- public void testSearchSuggestion_schemaFilter() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
-
- @Override
- @Test
- public void testSearchSuggestion_twoInstances() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
}
}
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 dd71fc5..3b44155 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
@@ -1837,15 +1837,22 @@
public void testGlobalQuery_propertyWeights() throws Exception {
assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
- // Schema registration
+ // RELEVANCE scoring depends on stats for the namespace+type of the scored document, namely
+ // the average document length. This average document length calculation is only updated
+ // when documents are added and when compaction runs. This means that old deleted
+ // documents of the same namespace and type combination *can* affect RELEVANCE scores
+ // through this channel.
+ // To avoid this, we use a unique namespace that will not be shared by any other test
+ // case or any other run of this test.
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
mDb2.setSchemaAsync(
new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+ String namespace = "propertyWeightsNamespace" + System.currentTimeMillis();
// Put two documents in separate databases.
AppSearchEmail emailDb1 =
- new AppSearchEmail.Builder("namespace", "id1")
+ new AppSearchEmail.Builder(namespace, "id1")
.setCreationTimestampMillis(1000)
.setSubject("foo")
.build();
@@ -1853,7 +1860,7 @@
new PutDocumentsRequest.Builder()
.addGenericDocuments(emailDb1).build()));
AppSearchEmail emailDb2 =
- new AppSearchEmail.Builder("namespace", "id2")
+ new AppSearchEmail.Builder(namespace, "id2")
.setCreationTimestampMillis(1000)
.setBody("foo")
.build();
@@ -1868,6 +1875,7 @@
.setPropertyWeights(AppSearchEmail.SCHEMA_TYPE,
ImmutableMap.of("subject",
2.0, "body", 0.5))
+ .addFilterNamespaces(namespace)
.build());
List<SearchResult> globalResults = retrieveAllSearchResults(searchResults);
@@ -1889,6 +1897,7 @@
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
.setOrder(SearchSpec.ORDER_DESCENDING)
+ .addFilterNamespaces(namespace)
.build());
List<SearchResult> resultsWithoutWeights =
retrieveAllSearchResults(searchResultsWithoutWeights);
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 038cd56..371de2d 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
@@ -202,6 +202,52 @@
}
@Test
+ public void testGetTypePropertyWeightsWithAdvancedRanking() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+ .setRankingStrategy("sum(this.propertyWeights())")
+ .setPropertyWeights("TypeA", ImmutableMap.of("property1", 1.0, "property2", 2.0))
+ .setPropertyWeights("TypeB", ImmutableMap.of("property1", 1.0, "property2"
+ + ".nested", 2.0))
+ .build();
+
+ Map<String, Map<String, Double>> typePropertyWeightsMap = searchSpec.getPropertyWeights();
+
+ assertThat(typePropertyWeightsMap.keySet())
+ .containsExactly("TypeA", "TypeB");
+ assertThat(typePropertyWeightsMap.get("TypeA")).containsExactly("property1", 1.0,
+ "property2", 2.0);
+ assertThat(typePropertyWeightsMap.get("TypeB")).containsExactly("property1", 1.0,
+ "property2.nested", 2.0);
+ }
+
+ @Test
+ public void testGetTypePropertyWeightPathsWithAdvancedRanking() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+ .setRankingStrategy("sum(this.propertyWeights())")
+ .setPropertyWeightPaths("TypeA",
+ ImmutableMap.of(new PropertyPath("property1"), 1.0,
+ new PropertyPath("property2"), 2.0))
+ .setPropertyWeightPaths("TypeB",
+ ImmutableMap.of(new PropertyPath("property1"), 1.0,
+ new PropertyPath("property2.nested"), 2.0))
+ .build();
+
+ Map<String, Map<PropertyPath, Double>> typePropertyWeightsMap =
+ searchSpec.getPropertyWeightPaths();
+
+ assertThat(typePropertyWeightsMap.keySet())
+ .containsExactly("TypeA", "TypeB");
+ assertThat(typePropertyWeightsMap.get("TypeA"))
+ .containsExactly(new PropertyPath("property1"), 1.0,
+ new PropertyPath("property2"), 2.0);
+ assertThat(typePropertyWeightsMap.get("TypeB"))
+ .containsExactly(new PropertyPath("property1"), 1.0,
+ new PropertyPath("property2.nested"), 2.0);
+ }
+
+ @Test
public void testSetPropertyWeights_nonPositiveWeight() {
SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder();
Map<String, Double> negativePropertyWeight = ImmutableMap.of("property", -1.0);
@@ -479,12 +525,13 @@
assertThat(e.getMessage()).isEqualTo("Attempting to rank based on joined documents, but"
+ " no JoinSpec provided");
+ JoinSpec joinSpec = new JoinSpec.Builder("childProp")
+ .setAggregationScoringStrategy(
+ JoinSpec.AGGREGATION_SCORING_SUM_RANKING_SIGNAL)
+ .build();
e = assertThrows(IllegalStateException.class, () -> new SearchSpec.Builder()
.setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
- .setJoinSpec(new JoinSpec.Builder("childProp")
- .setAggregationScoringStrategy(
- JoinSpec.AGGREGATION_SCORING_SUM_RANKING_SIGNAL)
- .build())
+ .setJoinSpec(joinSpec)
.build());
assertThat(e.getMessage()).isEqualTo("Aggregate scoring strategy has been set in the "
+ "nested JoinSpec, but ranking strategy is not "
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 5217017..69979c3 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
@@ -16,14 +16,11 @@
package androidx.appsearch.cts.app;
-import static androidx.appsearch.app.AppSearchResult.RESULT_INVALID_ARGUMENT;
-
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.appsearch.app.SearchSuggestionSpec;
-import androidx.appsearch.exceptions.AppSearchException;
import com.google.common.collect.ImmutableList;
@@ -68,12 +65,11 @@
@Test
public void testDocumentIdFilterMustMatchNamespaceFilter() throws Exception {
- AppSearchException e = assertThrows(AppSearchException.class,
+ IllegalStateException e = assertThrows(IllegalStateException.class,
() -> new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
.addFilterNamespaces("namespace1")
.addFilterDocumentIds("namespace2", ImmutableList.of("doc1"))
.build());
- assertThat(e.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
assertThat(e).hasMessageThat().contains("The namespace: namespace2 exists in the "
+ "document id filter but doesn't exist in the namespace filter.");
}
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 004a07e..0033e71 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
@@ -26,10 +26,12 @@
import androidx.annotation.NonNull;
import androidx.appsearch.annotation.Document;
import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.DocumentClassFactoryRegistry;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.Migrator;
import androidx.appsearch.app.PackageIdentifier;
import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.testutil.AppSearchEmail;
import androidx.collection.ArrayMap;
@@ -374,7 +376,7 @@
}
-// @exportToFramework:startStrip()
+ // @exportToFramework:startStrip()
@Document
static class Card {
@Document.Namespace
@@ -473,7 +475,7 @@
.addRequiredPermissionsForDocumentClassVisibility(Card.class,
ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR))
.addRequiredPermissionsForDocumentClassVisibility(Card.class,
- ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA));
+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA));
request = setSchemaRequestBuilder.build();
assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
@@ -833,4 +835,85 @@
assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getTokenizerType())
.isEqualTo(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_RFC822);
}
+
+ // @exportToFramework:startStrip()
+ @Document
+ static class Outer {
+ @Document.Id String mId;
+ @Document.Namespace String mNamespace;
+ @Document.DocumentProperty Middle mMiddle;
+ }
+
+ @Document
+ static class Middle {
+ @Document.Id String mId;
+ @Document.Namespace String mNamespace;
+ @Document.DocumentProperty Inner mInner;
+ }
+
+ @Document
+ static class Inner {
+ @Document.Id String mId;
+ @Document.Namespace String mNamespace;
+ @Document.StringProperty String mContents;
+ }
+
+ @Test
+ public void testNestedSchemas() throws AppSearchException {
+ SetSchemaRequest request = new SetSchemaRequest.Builder().addDocumentClasses(Outer.class)
+ .setForceOverride(true).build();
+ DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+
+ Set<AppSearchSchema> schemas = request.getSchemas();
+ assertThat(schemas).hasSize(3);
+ assertThat(schemas).contains(registry.getOrCreateFactory(Outer.class).getSchema());
+ assertThat(schemas).contains(registry.getOrCreateFactory(Middle.class).getSchema());
+ assertThat(schemas).contains(registry.getOrCreateFactory(Inner.class).getSchema());
+ }
+
+ @Document
+ static class Parent {
+ @Document.Id String mId;
+ @Document.Namespace String mNamespace;
+ @Document.DocumentProperty Person mPerson;
+ @Document.DocumentProperty Organization mOrganization;
+ }
+
+ @Document
+ static class Person {
+ @Document.Id String mId;
+ @Document.Namespace String mNamespace;
+ @Document.DocumentProperty Common mCommon;
+ }
+
+ @Document
+ static class Organization {
+ @Document.Id String mId;
+ @Document.Namespace String mNamespace;
+ @Document.DocumentProperty Common mCommon;
+ }
+
+ @Document
+ static class Common {
+ @Document.Id String mId;
+ @Document.Namespace String mNamespace;
+ @Document.StringProperty String mContents;
+ }
+
+
+ @Test
+ public void testNestedSchemasMultiplePaths() throws AppSearchException {
+ SetSchemaRequest request = new SetSchemaRequest.Builder().addDocumentClasses(Parent.class)
+ .setForceOverride(true).build();
+ DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+
+ Set<AppSearchSchema> schemas = request.getSchemas();
+ assertThat(schemas).hasSize(4);
+ assertThat(schemas).contains(registry.getOrCreateFactory(Common.class).getSchema());
+ assertThat(schemas).contains(registry.getOrCreateFactory(Organization.class).getSchema());
+ assertThat(schemas).contains(registry.getOrCreateFactory(Person.class).getSchema());
+ assertThat(schemas).contains(registry.getOrCreateFactory(Parent.class).getSchema());
+ }
+
+// @exportToFramework:endStrip()
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/CanIgnoreReturnValue.java b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/CanIgnoreReturnValue.java
new file mode 100644
index 0000000..7fa14d3
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/CanIgnoreReturnValue.java
@@ -0,0 +1,36 @@
+/*
+ * 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.annotation;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that the return value of the annotated API is ignorable.
+ *
+ * @hide
+ */
+@Documented
+@Target({METHOD, CONSTRUCTOR, TYPE})
+@Retention(CLASS)
+public @interface CanIgnoreReturnValue {}
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 04989eb..893a197 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
@@ -215,6 +215,28 @@
default AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
/**
+ * Configures how a property should be processed so that the document can be joined.
+ *
+ * <p>Properties configured with
+ * {@link AppSearchSchema.StringPropertyConfig#JOINABLE_VALUE_TYPE_QUALIFIED_ID} enable
+ * the documents to be joined with other documents that have the same qualified ID as the
+ * value of this field. (A qualified ID is a compact representation of the tuple <package
+ * name, database name, namespace, document ID> that uniquely identifies a document
+ * indexed in the AppSearch storage backend.) This property name can be specified as the
+ * child property expression in {@link androidx.appsearch.app.JoinSpec.Builder(String)} for
+ * join operations.
+ *
+ * <p>This attribute doesn't apply to properties of a repeated type (e.g., a list).
+ *
+ * <p>If not specified, defaults to
+ * {@link AppSearchSchema.StringPropertyConfig#JOINABLE_VALUE_TYPE_NONE}, which means the
+ * property can not be used in a child property expression to configure a
+ * {@link androidx.appsearch.app.JoinSpec.Builder(String)}.
+ */
+ @AppSearchSchema.StringPropertyConfig.JoinableValueType int joinableValueType()
+ default AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_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).
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 3cdf6ce..72dca86 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
@@ -17,6 +17,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.collection.ArrayMap;
import androidx.core.util.Preconditions;
@@ -138,6 +139,7 @@
* @param value An optional value to associate with the successful result of the operation
* being performed.
*/
+ @CanIgnoreReturnValue
@SuppressWarnings("MissingGetterMatchingBuilder") // See getSuccesses
@NonNull
public Builder<KeyType, ValueType> setSuccess(
@@ -161,6 +163,7 @@
* {@link AppSearchResult#getResultCode}.
* @param errorMessage An optional string describing the reason or nature of the failure.
*/
+ @CanIgnoreReturnValue
@SuppressWarnings("MissingGetterMatchingBuilder") // See getFailures
@NonNull
public Builder<KeyType, ValueType> setFailure(
@@ -181,6 +184,7 @@
* identifier from the input like an ID or name.
* @param result The result to associate with the key.
*/
+ @CanIgnoreReturnValue
@SuppressWarnings("MissingGetterMatchingBuilder") // See getAll
@NonNull
public Builder<KeyType, ValueType> setResult(
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 31b0a88..3ae1a52 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
@@ -52,6 +52,7 @@
RESULT_NOT_FOUND,
RESULT_INVALID_SCHEMA,
RESULT_SECURITY_ERROR,
+ RESULT_DENIED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ResultCode {}
@@ -95,6 +96,14 @@
/** The caller requested an operation it does not have privileges for. */
public static final int RESULT_SECURITY_ERROR = 8;
+ /**
+ * 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
+ public static final int RESULT_DENIED = 9;
+
private final @ResultCode int mResultCode;
@Nullable private final ValueType mResultValue;
@Nullable private final String mErrorMessage;
@@ -114,7 +123,8 @@
}
/** Returns one of the {@code RESULT} constants defined in {@link AppSearchResult}. */
- public @ResultCode int getResultCode() {
+ @ResultCode
+ public int getResultCode() {
return mResultCode;
}
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 8a05b92..5fbffd1 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
@@ -23,6 +23,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresFeature;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.exceptions.IllegalSchemaException;
import androidx.appsearch.util.BundleUtil;
import androidx.appsearch.util.IndentingStringBuilder;
@@ -172,6 +173,7 @@
}
/** Adds a property to the given type. */
+ @CanIgnoreReturnValue
@NonNull
public AppSearchSchema.Builder addProperty(@NonNull PropertyConfig propertyConfig) {
Preconditions.checkNotNull(propertyConfig);
@@ -215,10 +217,12 @@
/**
* Physical data-types of the contents of the property.
+ *
+ * <p>NOTE: The integer values of these constants must match the proto enum constants in
+ * com.google.android.icing.proto.PropertyConfigProto.DataType.Code.
+ *
* @hide
*/
- // NOTE: The integer values of these constants must match the proto enum constants in
- // com.google.android.icing.proto.PropertyConfigProto.DataType.Code.
@IntDef(value = {
DATA_TYPE_STRING,
DATA_TYPE_LONG,
@@ -262,10 +266,12 @@
/**
* 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
+ * com.google.android.icing.proto.PropertyConfigProto.Cardinality.Code.
+ *
* @hide
*/
- // NOTE: The integer values of these constants must match the proto enum constants in
- // com.google.android.icing.proto.PropertyConfigProto.Cardinality.Code.
@IntDef(value = {
CARDINALITY_REPEATED,
CARDINALITY_OPTIONAL,
@@ -375,14 +381,16 @@
*
* @hide
*/
- public @DataType int getDataType() {
+ @DataType
+ public int getDataType() {
return mBundle.getInt(DATA_TYPE_FIELD, -1);
}
/**
* Returns the cardinality of the property (whether it is optional, required or repeated).
*/
- public @Cardinality int getCardinality() {
+ @Cardinality
+ public int getCardinality() {
return mBundle.getInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL);
}
@@ -446,6 +454,7 @@
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.
@@ -480,10 +489,12 @@
/**
* Configures how tokens should be extracted from this property.
+ *
+ * <p>NOTE: The integer values of these constants must match the proto enum constants in
+ * com.google.android.icing.proto.IndexingConfig.TokenizerType.Code.
+ *
* @hide
*/
- // NOTE: The integer values of these constants must match the proto enum constants in
- // com.google.android.icing.proto.IndexingConfig.TokenizerType.Code.
@IntDef(value = {
TOKENIZER_TYPE_NONE,
TOKENIZER_TYPE_PLAIN,
@@ -592,29 +603,44 @@
}
/** Returns how the property is indexed. */
- public @IndexingType int getIndexingType() {
+ @IndexingType
+ public int getIndexingType() {
return mBundle.getInt(INDEXING_TYPE_FIELD);
}
/** Returns how this property is tokenized (split into words). */
- public @TokenizerType int getTokenizerType() {
+ @TokenizerType
+ public int getTokenizerType() {
return mBundle.getInt(TOKENIZER_TYPE_FIELD);
}
/**
* Returns how this property is going to be used to join documents from other schema types.
*/
- public @JoinableValueType int getJoinableValueType() {
+ @JoinableValueType
+ public int getJoinableValueType() {
return mBundle.getInt(JOINABLE_VALUE_TYPE_FIELD, 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);
+ }
+
/** Builder for {@link StringPropertyConfig}. */
public static final class Builder {
private final String mPropertyName;
- private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
- private @IndexingType int mIndexingType = INDEXING_TYPE_NONE;
- private @TokenizerType int mTokenizerType = TOKENIZER_TYPE_NONE;
- private @JoinableValueType int mJoinableValueType = JOINABLE_VALUE_TYPE_NONE;
+ @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 boolean mDeletionPropagation = false;
/** Creates a new {@link StringPropertyConfig.Builder}. */
public Builder(@NonNull String propertyName) {
@@ -627,6 +653,7 @@
* <p>If this method is not called, the default cardinality is
* {@link PropertyConfig#CARDINALITY_OPTIONAL}.
*/
+ @CanIgnoreReturnValue
@SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
@NonNull
public StringPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
@@ -643,6 +670,7 @@
* {@link StringPropertyConfig#INDEXING_TYPE_NONE}, so that it cannot be matched by
* queries.
*/
+ @CanIgnoreReturnValue
@NonNull
public StringPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
Preconditions.checkArgumentInRange(
@@ -662,6 +690,7 @@
* if {@link #setIndexingType} has been called with a value other than
* {@link StringPropertyConfig#INDEXING_TYPE_NONE}).
*/
+ @CanIgnoreReturnValue
@NonNull
public StringPropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) {
Preconditions.checkArgumentInRange(
@@ -676,6 +705,7 @@
* <p>If this method is not called, the default joinable value type is
* {@link StringPropertyConfig#JOINABLE_VALUE_TYPE_NONE}, so that it is not joinable.
*/
+ @CanIgnoreReturnValue
@NonNull
public StringPropertyConfig.Builder setJoinableValueType(
@JoinableValueType int joinableValueType) {
@@ -689,6 +719,25 @@
}
/**
+ * 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
@@ -704,6 +753,9 @@
if (mJoinableValueType == JOINABLE_VALUE_TYPE_QUALIFIED_ID) {
Preconditions.checkState(mCardinality != CARDINALITY_REPEATED, "Cannot set "
+ "JOINABLE_VALUE_TYPE_QUALIFIED_ID with CARDINALITY_REPEATED.");
+ } else {
+ Preconditions.checkState(!mDeletionPropagation, "Cannot set deletion "
+ + "propagation without setting a joinable value type");
}
Bundle bundle = new Bundle();
bundle.putString(NAME_FIELD, mPropertyName);
@@ -712,6 +764,7 @@
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);
}
}
@@ -807,15 +860,16 @@
}
/** Returns how the property is indexed. */
- public @IndexingType int getIndexingType() {
+ @IndexingType
+ public int getIndexingType() {
return mBundle.getInt(INDEXING_TYPE_FIELD, INDEXING_TYPE_NONE);
}
/** Builder for {@link LongPropertyConfig}. */
public static final class Builder {
private final String mPropertyName;
- private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
- private @IndexingType int mIndexingType = INDEXING_TYPE_NONE;
+ @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
+ @IndexingType private int mIndexingType = INDEXING_TYPE_NONE;
/** Creates a new {@link LongPropertyConfig.Builder}. */
public Builder(@NonNull String propertyName) {
@@ -828,6 +882,7 @@
* <p>If this method is not called, the default cardinality is
* {@link PropertyConfig#CARDINALITY_OPTIONAL}.
*/
+ @CanIgnoreReturnValue
@SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
@NonNull
public LongPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
@@ -844,6 +899,7 @@
* {@link LongPropertyConfig#INDEXING_TYPE_NONE}, so that it will not be indexed
* and cannot be matched by queries.
*/
+ @CanIgnoreReturnValue
@NonNull
public LongPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
Preconditions.checkArgumentInRange(
@@ -895,7 +951,7 @@
/** Builder for {@link DoublePropertyConfig}. */
public static final class Builder {
private final String mPropertyName;
- private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
+ @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
/** Creates a new {@link DoublePropertyConfig.Builder}. */
public Builder(@NonNull String propertyName) {
@@ -908,6 +964,7 @@
* <p>If this method is not called, the default cardinality is
* {@link PropertyConfig#CARDINALITY_OPTIONAL}.
*/
+ @CanIgnoreReturnValue
@SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
@NonNull
public DoublePropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
@@ -938,7 +995,7 @@
/** Builder for {@link BooleanPropertyConfig}. */
public static final class Builder {
private final String mPropertyName;
- private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
+ @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
/** Creates a new {@link BooleanPropertyConfig.Builder}. */
public Builder(@NonNull String propertyName) {
@@ -951,6 +1008,7 @@
* <p>If this method is not called, the default cardinality is
* {@link PropertyConfig#CARDINALITY_OPTIONAL}.
*/
+ @CanIgnoreReturnValue
@SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
@NonNull
public BooleanPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
@@ -981,7 +1039,7 @@
/** Builder for {@link BytesPropertyConfig}. */
public static final class Builder {
private final String mPropertyName;
- private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
+ @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
/** Creates a new {@link BytesPropertyConfig.Builder}. */
public Builder(@NonNull String propertyName) {
@@ -994,6 +1052,7 @@
* <p>If this method is not called, the default cardinality is
* {@link PropertyConfig#CARDINALITY_OPTIONAL}.
*/
+ @CanIgnoreReturnValue
@SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
@NonNull
public BytesPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
@@ -1047,7 +1106,7 @@
public static final class Builder {
private final String mPropertyName;
private final String mSchemaType;
- private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
+ @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
private boolean mShouldIndexNestedProperties = false;
/**
@@ -1071,6 +1130,7 @@
* <p>If this method is not called, the default cardinality is
* {@link PropertyConfig#CARDINALITY_OPTIONAL}.
*/
+ @CanIgnoreReturnValue
@SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
@NonNull
public DocumentPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
@@ -1087,6 +1147,7 @@
* <p>If false, the nested document's properties are not indexed regardless of its own
* schema.
*/
+ @CanIgnoreReturnValue
@NonNull
public DocumentPropertyConfig.Builder setShouldIndexNestedProperties(
boolean indexNestedProperties) {
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 16098ebc..e9679b7 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -19,7 +19,6 @@
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
import com.google.common.util.concurrent.ListenableFuture;
@@ -208,17 +207,14 @@
*
* <p>Search suggestions with the multiple term {@code suggestionQueryExpression} "org t", the
* suggested result will be "org term1" - The last token is completed by the suggested
- * String, even if it won't return any result.
+ * String.
*
- * <p>Search suggestions with operators. All operators will be considered as a normal term.
- * <ul>
- * <li>Search suggestions with the {@code suggestionQueryExpression} "term1 OR", the
- * suggested result is "term1 org".
- * <li>Search suggestions with the {@code suggestionQueryExpression} "term3 OR t", the
- * suggested result is "term3 OR term1".
- * <li>Search suggestions with the {@code suggestionQueryExpression} "content:t", the
- * suggested result is empty. It cannot find a document that contains the term "content:t".
- * </ul>
+ * <p>Operators in {@link #search} are supported.
+ * <p><b>NOTE:</b> Exclusion and Grouped Terms in the last term is not supported.
+ * <p>example: "apple -f": This Api will throw an
+ * {@link androidx.appsearch.exceptions.AppSearchException} with
+ * {@link AppSearchResult#RESULT_INVALID_ARGUMENT}.
+ * <p>example: "apple (f)": This Api will return an empty results.
*
* <p>Invalid example: All these input {@code suggestionQueryExpression} don't have a valid
* last token, AppSearch will return an empty result list.
@@ -229,10 +225,6 @@
* <li>"f " - Ending in trailing space.
* </ul>
*
- * <p>Property restrict query like "subject:f" is not supported in suggestion API. It will
- * return suggested String starting with "f" even if the term appears other than "subject"
- * property.
- *
* @param suggestionQueryExpression the non empty query string to search suggestions
* @param searchSuggestionSpec spec for setting document filters
* @return The pending result of performing this operation which resolves to a List of
@@ -241,15 +233,8 @@
* in {@link #search}.
*
* @see #search(String, SearchSpec)
- * <!--@exportToFramework:ifJetpack()-->@hide<!--@exportToFramework:else()-->
*/
- //TODO(b/227356108) Change the comment in this API after fix following issues.
- // 1: support property restrict tokenization, Example: [subject:car] will return ["cart",
- // "carburetor"] if AppSearch has documents contain those terms.
- // 2: support multiple terms, Example: [bar f] will return suggestions [bar foo] that could
- // be used to retrieve documents that contain both terms "bar" and "foo".
@NonNull
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
ListenableFuture<List<SearchSuggestionResult>> searchSuggestionAsync(
@NonNull String suggestionQueryExpression,
@NonNull SearchSuggestionSpec searchSuggestionSpec);
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactory.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactory.java
index bd03221..fd5987e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactory.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactory.java
@@ -19,6 +19,8 @@
import androidx.annotation.NonNull;
import androidx.appsearch.exceptions.AppSearchException;
+import java.util.List;
+
/**
* An interface for factories which can convert between instances of classes annotated with
* \@{@link androidx.appsearch.annotation.Document} and instances of {@link GenericDocument}.
@@ -39,6 +41,13 @@
AppSearchSchema getSchema() throws AppSearchException;
/**
+ * Returns dependent document classes used in this document class. This is useful as we can set
+ * dependent schemas without requiring clients to explicitly set all dependent schemas.
+ */
+ @NonNull
+ List<Class<?>> getNestedDocumentClasses() throws AppSearchException;
+
+ /**
* Converts an instance of the class annotated with
* \@{@link androidx.appsearch.annotation.Document} into a
* {@link androidx.appsearch.app.GenericDocument}.
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/FeatureConstants.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/FeatureConstants.java
new file mode 100644
index 0000000..fc3218f
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/FeatureConstants.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/**
+ * A class that encapsulates all feature constants that are accessible in AppSearch framework.
+ *
+ * <p>All fields in this class is referring in {@link Features}. If you add/remove any field in this
+ * class, you should also change {@link Features}.
+ * @see Features
+ * @hide
+ */
+public interface FeatureConstants {
+ /** Feature constants for {@link Features#NUMERIC_SEARCH}. */
+ String NUMERIC_SEARCH = "NUMERIC_SEARCH";
+
+ /** Feature constants for {@link Features#VERBATIM_SEARCH}. */
+ String VERBATIM_SEARCH = "VERBATIM_SEARCH";
+
+ /** Feature constants for {@link Features#LIST_FILTER_QUERY_LANGUAGE}. */
+ String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
+}
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 4a4af6c..4686ea7 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
@@ -26,6 +26,8 @@
* the feature will be available forever on that AppSearch storage implementation, at that
* Android API level, on that device.
*/
+
+// @exportToFramework:copyToPath(testing/testutils/src/android/app/appsearch/testutil/external/Features.java)
public interface Features {
/**
@@ -78,7 +80,7 @@
* {@link AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE} and all other numeric search
* features.
*/
- String NUMERIC_SEARCH = "NUMERIC_SEARCH";
+ String NUMERIC_SEARCH = FeatureConstants.NUMERIC_SEARCH;
/**
* Feature for {@link #isFeatureSupported(String)}. This feature covers
@@ -88,7 +90,7 @@
*
* <p>Ex. '"foo/bar" OR baz' will ensure that 'foo/bar' is treated as a single 'verbatim' token.
*/
- String VERBATIM_SEARCH = "VERBATIM_SEARCH";
+ String VERBATIM_SEARCH = FeatureConstants.VERBATIM_SEARCH;
/**
* Feature for {@link #isFeatureSupported(String)}. This feature covers the
@@ -115,7 +117,13 @@
* for example, the query "(subject:foo OR body:foo) (subject:bar OR body:bar)"
* could be rewritten as "termSearch(\"foo bar\", createList(\"subject\", \"bar\"))"
*/
- String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
+ String LIST_FILTER_QUERY_LANGUAGE = FeatureConstants.LIST_FILTER_QUERY_LANGUAGE;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers
+ * {@link SearchSpec#GROUPING_TYPE_PER_SCHEMA}
+ */
+ String SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA = "SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA";
/** Feature for {@link #isFeatureSupported(String)}. This feature covers
* {@link SearchSpec.Builder#setPropertyWeights}.
@@ -136,6 +144,18 @@
String JOIN_SPEC_AND_QUALIFIED_ID = "JOIN_SPEC_AND_QUALIFIED_ID";
/**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers
+ * {@link AppSearchSession#searchSuggestionAsync}.
+ */
+ 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";
+
+ /**
* 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 5dd5a6c..992bb0d 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -25,6 +25,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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;
@@ -426,7 +427,7 @@
// paths we return the bare document Bundle in this particular case.
Parcelable[] bundles = (Parcelable[]) currentElementValue;
if (index < bundles.length) {
- extractedValue = (Bundle) bundles[index];
+ extractedValue = bundles[index];
}
} else {
throw new IllegalStateException("Unsupported value type: "
@@ -1127,6 +1128,7 @@
* <p>The number of namespaces per app should be kept small for efficiency reasons.
* <!--@exportToFramework:hide-->
*/
+ @CanIgnoreReturnValue
@NonNull
public BuilderType setNamespace(@NonNull String namespace) {
Preconditions.checkNotNull(namespace);
@@ -1142,6 +1144,7 @@
* <p>Document IDs are unique within a namespace.
* <!--@exportToFramework:hide-->
*/
+ @CanIgnoreReturnValue
@NonNull
public BuilderType setId(@NonNull String id) {
Preconditions.checkNotNull(id);
@@ -1157,6 +1160,7 @@
* {@link AppSearchSchema} object previously provided to {@link AppSearchSession#setSchemaAsync}.
* <!--@exportToFramework:hide-->
*/
+ @CanIgnoreReturnValue
@NonNull
public BuilderType setSchemaType(@NonNull String schemaType) {
Preconditions.checkNotNull(schemaType);
@@ -1177,7 +1181,9 @@
* <p>Any non-negative integer can be used a score. By default, scores are set to 0.
*
* @param score any non-negative {@code int} representing the document's score.
+ * @throws IllegalArgumentException if the score is negative.
*/
+ @CanIgnoreReturnValue
@NonNull
public BuilderType setScore(@IntRange(from = 0, to = Integer.MAX_VALUE) int score) {
if (score < 0) {
@@ -1198,6 +1204,7 @@
*
* @param creationTimestampMillis a creation timestamp in milliseconds.
*/
+ @CanIgnoreReturnValue
@NonNull
public BuilderType setCreationTimestampMillis(
/*@exportToFramework:CurrentTimeMillisLong*/ long creationTimestampMillis) {
@@ -1219,7 +1226,9 @@
* called.
*
* @param ttlMillis a non-negative duration in milliseconds.
+ * @throws IllegalArgumentException if ttlMillis is negative.
*/
+ @CanIgnoreReturnValue
@NonNull
public BuilderType setTtlMillis(long ttlMillis) {
if (ttlMillis < 0) {
@@ -1241,6 +1250,7 @@
* @throws IllegalArgumentException if no values are provided, or if a passed in
* {@code String} is {@code null} or "".
*/
+ @CanIgnoreReturnValue
@NonNull
public BuilderType setPropertyString(@NonNull String name, @NonNull String... values) {
Preconditions.checkNotNull(name);
@@ -1260,6 +1270,7 @@
* @param values the {@code boolean} values of the property.
* @throws IllegalArgumentException if the name is empty or {@code null}.
*/
+ @CanIgnoreReturnValue
@NonNull
public BuilderType setPropertyBoolean(@NonNull String name, @NonNull boolean... values) {
Preconditions.checkNotNull(name);
@@ -1279,6 +1290,7 @@
* @param values the {@code long} values of the property.
* @throws IllegalArgumentException if the name is empty or {@code null}.
*/
+ @CanIgnoreReturnValue
@NonNull
public BuilderType setPropertyLong(@NonNull String name, @NonNull long... values) {
Preconditions.checkNotNull(name);
@@ -1298,6 +1310,7 @@
* @param values the {@code double} values of the property.
* @throws IllegalArgumentException if the name is empty or {@code null}.
*/
+ @CanIgnoreReturnValue
@NonNull
public BuilderType setPropertyDouble(@NonNull String name, @NonNull double... values) {
Preconditions.checkNotNull(name);
@@ -1317,6 +1330,7 @@
* @throws IllegalArgumentException if no values are provided, or if a passed in
* {@code byte[]} is {@code null}, or if name is empty.
*/
+ @CanIgnoreReturnValue
@NonNull
public BuilderType setPropertyBytes(@NonNull String name, @NonNull byte[]... values) {
Preconditions.checkNotNull(name);
@@ -1338,6 +1352,7 @@
* {@link GenericDocument} is {@code null}, or if name
* is empty.
*/
+ @CanIgnoreReturnValue
@NonNull
public BuilderType setPropertyDocument(
@NonNull String name, @NonNull GenericDocument... values) {
@@ -1356,6 +1371,7 @@
* @param name The name of the property to clear.
* <!--@exportToFramework:hide-->
*/
+ @CanIgnoreReturnValue
@NonNull
public BuilderType clearProperty(@NonNull String name) {
Preconditions.checkNotNull(name);
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 d7ff30b..2bee987 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
@@ -134,6 +135,7 @@
}
/** Adds one or more document IDs to the request. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addIds(@NonNull String... ids) {
Preconditions.checkNotNull(ids);
@@ -142,6 +144,7 @@
}
/** Adds a collection of IDs to the request. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addIds(@NonNull Collection<String> ids) {
Preconditions.checkNotNull(ids);
@@ -166,6 +169,7 @@
*
* @see SearchSpec.Builder#addProjectionPaths
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addProjection(
@NonNull String schemaType, @NonNull Collection<String> propertyPaths) {
@@ -197,6 +201,7 @@
*
* @see SearchSpec.Builder#addProjectionPaths
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addProjectionPaths(
@NonNull String schemaType, @NonNull Collection<PropertyPath> propertyPaths) {
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 3475576..b00e904 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
@@ -24,6 +24,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresFeature;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
@@ -276,6 +277,7 @@
*
* <p>Default version is 0
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setVersion(@IntRange(from = 0) int version) {
resetIfBuilt();
@@ -284,6 +286,7 @@
}
/** Adds one {@link AppSearchSchema} to the schema list. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addSchema(@NonNull AppSearchSchema schema) {
Preconditions.checkNotNull(schema);
@@ -300,6 +303,7 @@
* {@link GetSchemaResponse}, which won't be displayed by system.
*/
// Getter getSchemaTypesNotDisplayedBySystem returns plural objects.
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder addSchemaTypeNotDisplayedBySystem(@NonNull String schemaType) {
@@ -331,6 +335,7 @@
* schema type.
*/
// Getter getSchemaTypesVisibleToPackages returns a map contains all schema types.
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder setSchemaTypeVisibleToPackages(
@@ -378,6 +383,7 @@
* the given schema.
*/
// Getter getRequiredPermissionsForSchemaTypeVisibility returns a map for all schemaTypes.
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder setRequiredPermissionsForSchemaTypeVisibility(
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 b5dcd55..5476a5c 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java
@@ -21,6 +21,7 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.core.util.Preconditions;
import java.lang.annotation.Retention;
@@ -32,7 +33,7 @@
* <p> Joins are only possible for matching on the qualified id of an outer document and a
* property value within a subquery document. In the subquery documents, these values may be
* referred to with a property path such as "email.recipient.id" or "entityId" or a property
- * expression. One such property expression is {@link #QUALIFIED_ID}, which refers to the
+ * expression. One such property expression is "this.qualifiedId()", which refers to the
* document's combined package, database, namespace, and id.
*
* <p> Take these outer query and subquery results for example:
@@ -97,7 +98,10 @@
*
* <p> For instance, if a document with an id of "id1" exists in the namespace "ns" within
* the database "db" created by package "pkg", this would evaluate to "pkg$db/ns#id1".
+ *
+ * <!--@exportToFramework:hide-->
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final String QUALIFIED_ID = "this.qualifiedId()";
/**
@@ -204,7 +208,8 @@
*
* @see SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE
*/
- public @AggregationScoringStrategy int getAggregationScoringStrategy() {
+ @AggregationScoringStrategy
+ public int getAggregationScoringStrategy() {
return mBundle.getInt(AGGREGATION_SCORING_STRATEGY);
}
@@ -218,7 +223,7 @@
private SearchSpec mNestedSearchSpec = EMPTY_SEARCH_SPEC;
private final String mChildPropertyExpression;
private int mMaxJoinedResultCount = DEFAULT_MAX_JOINED_RESULT_COUNT;
- private @AggregationScoringStrategy int mAggregationScoringStrategy =
+ @AggregationScoringStrategy private int mAggregationScoringStrategy =
AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL;
/**
@@ -262,6 +267,7 @@
*/
@SuppressWarnings("MissingGetterMatchingBuilder")
// See getNestedQuery & getNestedSearchSpec
+ @CanIgnoreReturnValue
@NonNull
public Builder setNestedSearch(@NonNull String nestedQuery,
@NonNull SearchSpec nestedSearchSpec) {
@@ -277,6 +283,7 @@
* Sets the max amount of {@link SearchResults} to join to the parent document, with a
* default of 10 SearchResults.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setMaxJoinedResultCount(int maxJoinedResultCount) {
mMaxJoinedResultCount = maxJoinedResultCount;
@@ -292,6 +299,7 @@
*
* @see SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setAggregationScoringStrategy(
@AggregationScoringStrategy int aggregationScoringStrategy) {
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 dff69b2..6edf85e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
@@ -19,6 +19,7 @@
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.core.util.Preconditions;
@@ -58,6 +59,7 @@
private boolean mBuilt = false;
/** Adds one or more {@link GenericDocument} objects to the request. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addGenericDocuments(@NonNull GenericDocument... documents) {
Preconditions.checkNotNull(documents);
@@ -66,6 +68,7 @@
}
/** Adds a collection of {@link GenericDocument} objects to the request. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addGenericDocuments(
@NonNull Collection<? extends GenericDocument> documents) {
@@ -87,6 +90,7 @@
*/
// Merged list available from getGenericDocuments()
@SuppressLint("MissingGetterMatchingBuilder")
+ @CanIgnoreReturnValue
@NonNull
public Builder addDocuments(@NonNull Object... documents) throws AppSearchException {
Preconditions.checkNotNull(documents);
@@ -105,6 +109,7 @@
*/
// Merged list available from getGenericDocuments()
@SuppressLint("MissingGetterMatchingBuilder")
+ @CanIgnoreReturnValue
@NonNull
public Builder addDocuments(@NonNull Collection<?> documents) throws AppSearchException {
Preconditions.checkNotNull(documents);
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 38be17a..40cd591 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
@@ -17,6 +17,7 @@
package androidx.appsearch.app;
import androidx.annotation.NonNull;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
@@ -64,6 +65,7 @@
}
/** Adds one or more document IDs to the request. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addIds(@NonNull String... ids) {
Preconditions.checkNotNull(ids);
@@ -72,6 +74,7 @@
}
/** Adds a collection of IDs to the request. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addIds(@NonNull Collection<String> ids) {
Preconditions.checkNotNull(ids);
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java
index 12422eb..d873a99 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java
@@ -17,6 +17,7 @@
package androidx.appsearch.app;
import androidx.annotation.NonNull;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.core.util.Preconditions;
/**
@@ -123,6 +124,7 @@
* <p>If unset, this defaults to the current timestamp at the time that the
* {@link ReportSystemUsageRequest} is constructed.
*/
+ @CanIgnoreReturnValue
@NonNull
public ReportSystemUsageRequest.Builder setUsageTimestampMillis(
/*@exportToFramework:CurrentTimeMillisLong*/ long usageTimestampMillis) {
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 14b70c7..567cd40 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
@@ -17,6 +17,7 @@
package androidx.appsearch.app;
import androidx.annotation.NonNull;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.core.util.Preconditions;
/**
@@ -89,6 +90,7 @@
* <p>If unset, this defaults to the current timestamp at the time that the
* {@link ReportUsageRequest} is constructed.
*/
+ @CanIgnoreReturnValue
@NonNull
public ReportUsageRequest.Builder setUsageTimestampMillis(
/*@exportToFramework:CurrentTimeMillisLong*/ long usageTimestampMillis) {
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 32e4507..efda86b 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
@@ -22,6 +22,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresFeature;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;
@@ -244,6 +245,7 @@
* @throws AppSearchException if an error occurs converting a document class into a
* {@link GenericDocument}.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setDocument(@NonNull Object document) throws AppSearchException {
Preconditions.checkNotNull(document);
@@ -253,6 +255,7 @@
// @exportToFramework:endStrip()
/** Sets the document which matched. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setGenericDocument(@NonNull GenericDocument document) {
Preconditions.checkNotNull(document);
@@ -262,6 +265,7 @@
}
/** Adds another match to this SearchResult. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addMatchInfo(@NonNull MatchInfo matchInfo) {
Preconditions.checkState(
@@ -274,6 +278,7 @@
}
/** Sets the ranking signal of the matched document in this SearchResult. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setRankingSignal(double rankingSignal) {
resetIfBuilt();
@@ -285,6 +290,7 @@
* Adds a {@link SearchResult} that was joined by the {@link JoinSpec}.
* @param joinedResult The joined SearchResult to add.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addJoinedResult(@NonNull SearchResult joinedResult) {
resetIfBuilt();
@@ -645,6 +651,7 @@
}
/** Sets the exact {@link MatchRange} corresponding to the given entry. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setExactMatchRange(@NonNull MatchRange matchRange) {
mExactMatchRange = Preconditions.checkNotNull(matchRange);
@@ -653,6 +660,7 @@
/** Sets the submatch {@link MatchRange} corresponding to the given entry. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setSubmatchRange(@NonNull MatchRange matchRange) {
mSubmatchRange = Preconditions.checkNotNull(matchRange);
@@ -660,6 +668,7 @@
}
/** Sets the snippet {@link MatchRange} corresponding to the given entry. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setSnippetRange(@NonNull MatchRange matchRange) {
mSnippetRange = Preconditions.checkNotNull(matchRange);
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 9f951fd..7b7a436 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
@@ -25,6 +25,7 @@
import androidx.annotation.Nullable;
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.util.BundleUtil;
@@ -188,23 +189,33 @@
*/
@IntDef(flag = true, value = {
GROUPING_TYPE_PER_PACKAGE,
- GROUPING_TYPE_PER_NAMESPACE
+ GROUPING_TYPE_PER_NAMESPACE,
+ GROUPING_TYPE_PER_SCHEMA
})
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Retention(RetentionPolicy.SOURCE)
public @interface GroupingType {
}
-
/**
* Results should be grouped together by package for the purpose of enforcing a limit on the
* number of results returned per package.
*/
- public static final int GROUPING_TYPE_PER_PACKAGE = 0b01;
+ public static final int GROUPING_TYPE_PER_PACKAGE = 1 << 0;
/**
* Results should be grouped together by namespace for the purpose of enforcing a limit on the
* number of results returned per namespace.
*/
- public static final int GROUPING_TYPE_PER_NAMESPACE = 0b10;
+ public static final int GROUPING_TYPE_PER_NAMESPACE = 1 << 1;
+ /**
+ * 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:startStrip()
+ @RequiresFeature(
+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+ name = Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA)
+ // @exportToFramework:endStrip()
+ public static final int GROUPING_TYPE_PER_SCHEMA = 1 << 2;
private final Bundle mBundle;
@@ -227,7 +238,8 @@
}
/** Returns how the query terms should match terms in the index. */
- public @TermMatch int getTermMatch() {
+ @TermMatch
+ public int getTermMatch() {
return mBundle.getInt(TERM_MATCH_TYPE_FIELD, -1);
}
@@ -281,12 +293,14 @@
}
/** Returns the ranking strategy. */
- public @RankingStrategy int getRankingStrategy() {
+ @RankingStrategy
+ public int getRankingStrategy() {
return mBundle.getInt(RANKING_STRATEGY_FIELD);
}
/** Returns the order of returned search results (descending or ascending). */
- public @Order int getOrder() {
+ @Order
+ public int getOrder() {
return mBundle.getInt(ORDER_FIELD);
}
@@ -415,7 +429,8 @@
* Get the type of grouping limit to apply, or 0 if {@link Builder#setResultGrouping} was not
* called.
*/
- public @GroupingType int getResultGroupingTypeFlags() {
+ @GroupingType
+ public int getResultGroupingTypeFlags() {
return mBundle.getInt(RESULT_GROUPING_TYPE_FLAGS);
}
@@ -454,21 +469,21 @@
* Returns whether the {@link Features#NUMERIC_SEARCH} feature is enabled.
*/
public boolean isNumericSearchEnabled() {
- return getEnabledFeatures().contains(Features.NUMERIC_SEARCH);
+ return getEnabledFeatures().contains(FeatureConstants.NUMERIC_SEARCH);
}
/**
* Returns whether the {@link Features#VERBATIM_SEARCH} feature is enabled.
*/
public boolean isVerbatimSearchEnabled() {
- return getEnabledFeatures().contains(Features.VERBATIM_SEARCH);
+ return getEnabledFeatures().contains(FeatureConstants.VERBATIM_SEARCH);
}
/**
* Returns whether the {@link Features#LIST_FILTER_QUERY_LANGUAGE} feature is enabled.
*/
public boolean isListFilterQueryLanguageEnabled() {
- return getEnabledFeatures().contains(Features.LIST_FILTER_QUERY_LANGUAGE);
+ return getEnabledFeatures().contains(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE);
}
/**
@@ -493,13 +508,13 @@
private Bundle mTypePropertyWeights = new Bundle();
private int mResultCountPerPage = DEFAULT_NUM_PER_PAGE;
- private @TermMatch int mTermMatchType = TERM_MATCH_PREFIX;
+ @TermMatch private int mTermMatchType = TERM_MATCH_PREFIX;
private int mSnippetCount = 0;
private int mSnippetCountPerProperty = MAX_SNIPPET_PER_PROPERTY_COUNT;
private int mMaxSnippetSize = 0;
- private @RankingStrategy int mRankingStrategy = RANKING_STRATEGY_NONE;
- private @Order int mOrder = ORDER_DESCENDING;
- private @GroupingType int mGroupingTypeFlags = 0;
+ @RankingStrategy private int mRankingStrategy = RANKING_STRATEGY_NONE;
+ @Order private int mOrder = ORDER_DESCENDING;
+ @GroupingType private int mGroupingTypeFlags = 0;
private int mGroupingLimit = 0;
private JoinSpec mJoinSpec;
private String mAdvancedRankingExpression = "";
@@ -511,6 +526,7 @@
* <p>If this method is not called, the default term match type is
* {@link SearchSpec#TERM_MATCH_PREFIX}.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setTermMatch(@TermMatch int termMatchType) {
Preconditions.checkArgumentInRange(termMatchType, TERM_MATCH_EXACT_ONLY,
@@ -526,6 +542,7 @@
*
* <p>If unset, the query will search over all schema types.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterSchemas(@NonNull String... schemas) {
Preconditions.checkNotNull(schemas);
@@ -539,6 +556,7 @@
*
* <p>If unset, the query will search over all schema types.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterSchemas(@NonNull Collection<String> schemas) {
Preconditions.checkNotNull(schemas);
@@ -555,9 +573,11 @@
*
* <p>If unset, the query will search over all schema types.
*
+ * <p>Merged list available from {@link #getFilterSchemas()}.
+ *
* @param documentClasses classes annotated with {@link Document}.
*/
- // Merged list available from getFilterSchemas
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder addFilterDocumentClasses(
@@ -583,9 +603,11 @@
*
* <p>If unset, the query will search over all schema types.
*
+ * <p>Merged list available from {@link #getFilterSchemas()}.
+ *
* @param documentClasses classes annotated with {@link Document}.
*/
- // Merged list available from getFilterSchemas()
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder addFilterDocumentClasses(@NonNull Class<?>... documentClasses)
@@ -601,6 +623,7 @@
* have the specified namespaces.
* <p>If unset, the query will search over all namespaces.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterNamespaces(@NonNull String... namespaces) {
Preconditions.checkNotNull(namespaces);
@@ -613,6 +636,7 @@
* have the specified namespaces.
* <p>If unset, the query will search over all namespaces.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterNamespaces(@NonNull Collection<String> namespaces) {
Preconditions.checkNotNull(namespaces);
@@ -629,6 +653,7 @@
* If package names are specified which caller doesn't have access to, then those package
* names will be ignored.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterPackageNames(@NonNull String... packageNames) {
Preconditions.checkNotNull(packageNames);
@@ -644,6 +669,7 @@
* If package names are specified which caller doesn't have access to, then those package
* names will be ignored.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterPackageNames(@NonNull Collection<String> packageNames) {
Preconditions.checkNotNull(packageNames);
@@ -657,6 +683,7 @@
*
* <p>The default number of results per page is 10.
*/
+ @CanIgnoreReturnValue
@NonNull
public SearchSpec.Builder setResultCountPerPage(
@IntRange(from = 0, to = MAX_NUM_PER_PAGE) int resultCountPerPage) {
@@ -668,6 +695,7 @@
}
/** Sets ranking strategy for AppSearch results. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setRankingStrategy(@RankingStrategy int rankingStrategy) {
Preconditions.checkArgumentInRange(rankingStrategy, RANKING_STRATEGY_NONE,
@@ -690,52 +718,41 @@
* <p>Numeric literals, arithmetic operators, mathematical functions, and document-based
* functions are supported to build expressions.
*
- * <p>The following are examples of numeric literals:
- * <ul>
- * <li>Integer
- * <p>Example: 0, 1, 2, 13
- * <li>Floating-point number
- * <p>Example: 0.333, 0.5, 123.456
- * <li>Negative number
- * <p>Example: -5, -10.5, -100.123
- * </ul>
- *
* <p>The following are supported arithmetic operators:
* <ul>
* <li>Addition(+)
- * <p>Example: "1 + 1" will be evaluated to 2.
* <li>Subtraction(-)
- * <p>Example: "2 - 1.5" will be evaluated to 0.5.
* <li>Multiplication(*)
- * <p>Example: "2 * -2" will be evaluated to -4.
- * <li>Division(/)
- * <p>Example: "5 / 2" will be evaluated to 2.5.
+ * <li>Floating Point Division(/)
* </ul>
*
- * <p>Multiplication and division have higher precedences than addition and subtraction,
- * but multiplication has the same precedence as division, and addition has the same
- * precedence as subtraction. Parentheses are supported to change precedences.
+ * <p>Operator precedences are compliant with the Java Language, and parentheses are
+ * supported. For example, "2.2 + (3 - 4) / 2" evaluates to 1.7.
*
- * <p>For example:
- * <ul>
- * <li>"2 + 3 - 4 * 5" will be evaluated to -15
- * <li>"(2 + 3) - (4 * 5)" will be evaluated to -15
- * <li>"2 + (3 - 4) * 5" will be evaluated to -3
- * </ul>
- *
- * <p>The following are supported mathematical functions:
+ * <p>The following are supported basic mathematical functions:
* <ul>
* <li>log(x) - the natural log of x
* <li>log(x, y) - the log of y with base x
* <li>pow(x, y) - x to the power of y
- * <li>max(v1, v2, ..., vn) with n > 0 - the maximum value among v1, ..., vn
- * <li>min(v1, v2, ..., vn) with n > 0 - the minimum value among v1, ..., vn
- * <li>sqrt(x) - the square root of x
- * <li>abs(x) - the absolute value of x
- * <li>sin(x), cos(x), tan(x) - trigonometric functions of x
+ * <li>sqrt(x)
+ * <li>abs(x)
+ * <li>sin(x), cos(x), tan(x)
* <li>Example: "max(abs(-100), 10) + pow(2, 10)" will be evaluated to 1124
* </ul>
*
+ * <p>The following variadic mathematical functions are supported, with n > 0. They also
+ * accept list value parameters. For example, if V is a value of list type, we can call
+ * sum(V) to get the sum of all the values in V. List literals are not supported, so a
+ * value of list type can only be constructed as a return value of some particular
+ * document-based functions.
+ * <ul>
+ * <li>max(v1, v2, ..., vn) or max(V)
+ * <li>min(v1, v2, ..., vn) or min(V)
+ * <li>len(v1, v2, ..., vn) or len(V)
+ * <li>sum(v1, v2, ..., vn) or sum(V)
+ * <li>avg(v1, v2, ..., vn) or avg(V)
+ * </ul>
+ *
* <p>Document-based functions must be called via "this", which represents the current
* document being scored. The following are supported document-based functions:
* <ul>
@@ -754,27 +771,59 @@
* document, where type must be evaluated to an integer from 1 to 2. Type 1 refers to
* usages reported by {@link AppSearchSession#reportUsageAsync}, and type 2 refers to
* usages reported by {@link GlobalSearchSession#reportSystemUsageAsync}.
+ * <li>this.childrenScores()
+ * <p>Returns a list of children document scores. Currently, a document can only be a
+ * child of another document in the context of joins. If this function is called
+ * without the Join API enabled, a type error will be raised.
+ * <li>this.propertyWeights()
+ * <p>Returns a list of the normalized weights of the matched properties for the
+ * 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.
* </ul>
*
* <p>Some errors may occur when using advanced ranking.
+ *
+ * <p>Syntax Error: the expression violates the syntax of the advanced ranking language.
+ * Below are some examples.
* <ul>
- * <li>Syntax Error: the expression violates the syntax of the advanced ranking
- * language, such as unbalanced parenthesis.
- * <li>Type Error: the expression fails a static type check, such as getting the wrong
- * number of arguments for a function.
- * <li>Evaluation Error: an error occurred while evaluating the value of the
- * expression, such as getting a non-finite value in the middle of evaluation.
- * Expressions like "1 / 0" and "log(0) fall into this category.
+ * <li>"1 + " - missing operand
+ * <li>"2 * (1 + 2))" - unbalanced parenthesis
+ * <li>"2 ^ 3" - unknown operator
+ * </ul>
+ *
+ * <p>Type Error: the expression fails a static type check. Below are some examples.
+ * <ul>
+ * <li>"sin(2, 3)" - wrong number of arguments for the sin function
+ * <li>"this.childrenScores() + 1" - cannot add a list with a number
+ * <li>"this.propertyWeights()" - the final type of the overall expression cannot be
+ * a list, which can be fixed by "max(this.propertyWeights())"
+ * <li>"abs(this.propertyWeights())" - the abs function does not support list type
+ * arguments
+ * <li>"print(2)" - unknown function
+ * </ul>
+ *
+ * <p>Evaluation Error: an error occurred while evaluating the value of the expression.
+ * Below are some examples.
+ * <ul>
+ * <li>"1 / 0", "log(0)", "1 + sqrt(-1)" - getting a non-finite value in the middle
+ * of evaluation
+ * <li>"this.usageCount(1 + 0.5)" - expect the argument to be an integer. Note that
+ * this is not a type error and "this.usageCount(1.5 + 1/2)" can succeed without any
+ * issues
+ * <li>"this.documentScore()" - in case of an IO error, this will be an evaluation error
* </ul>
*
* <p>Syntax errors and type errors will fail the entire search and will cause
- * {@link SearchResults#getNextPageAsync()} to throw an {@link AppSearchException}.
+ * {@link SearchResults#getNextPageAsync} to throw an {@link AppSearchException} with the
+ * result code of {@link AppSearchResult#RESULT_INVALID_ARGUMENT}.
* <p>Evaluation errors will result in the offending documents receiving the default score.
* For {@link #ORDER_DESCENDING}, the default score will be 0, for
* {@link #ORDER_ASCENDING} the default score will be infinity.
*
* @param advancedRankingExpression a non-empty string representing the ranking expression.
*/
+ @CanIgnoreReturnValue
@NonNull
// @exportToFramework:startStrip()
@RequiresFeature(
@@ -795,6 +844,7 @@
*
* <p>This order field will be ignored if RankingStrategy = {@code RANKING_STRATEGY_NONE}.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setOrder(@Order int order) {
Preconditions.checkArgumentInRange(order, ORDER_DESCENDING, ORDER_ASCENDING,
@@ -814,6 +864,7 @@
* <p>If set to 0 (default), snippeting is disabled and the list returned from
* {@link SearchResult#getMatchInfos} will be empty.
*/
+ @CanIgnoreReturnValue
@NonNull
public SearchSpec.Builder setSnippetCount(
@IntRange(from = 0, to = MAX_SNIPPET_COUNT) int snippetCount) {
@@ -834,6 +885,7 @@
* <p>The default behavior is to snippet all matches a property contains, up to the maximum
* value of 10,000.
*/
+ @CanIgnoreReturnValue
@NonNull
public SearchSpec.Builder setSnippetCountPerProperty(
@IntRange(from = 0, to = MAX_SNIPPET_PER_PROPERTY_COUNT)
@@ -857,6 +909,7 @@
* <p>Ex. {@code maxSnippetSize} = 16. "foo bar baz bat rat" with a query of "baz" will
* return a window of "bar baz bat" which is only 11 bytes long.
*/
+ @CanIgnoreReturnValue
@NonNull
public SearchSpec.Builder setMaxSnippetSize(
@IntRange(from = 0, to = MAX_SNIPPET_SIZE_LIMIT) int maxSnippetSize) {
@@ -878,6 +931,7 @@
* @param schema a string corresponding to the schema to add projections to.
* @param propertyPaths the projections to add.
*/
+ @CanIgnoreReturnValue
@NonNull
public SearchSpec.Builder addProjection(
@NonNull String schema, @NonNull Collection<String> propertyPaths) {
@@ -956,6 +1010,7 @@
* @param schema a string corresponding to the schema to add projections to.
* @param propertyPaths the projections to add.
*/
+ @CanIgnoreReturnValue
@NonNull
public SearchSpec.Builder addProjectionPaths(
@NonNull String schema, @NonNull Collection<PropertyPath> propertyPaths) {
@@ -981,6 +1036,7 @@
* add projections to.
* @param propertyPaths the projections to add.
*/
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder") // Projections available from getProjections
@NonNull
public SearchSpec.Builder addProjectionsForDocumentClass(
@@ -1001,6 +1057,7 @@
* add projections to.
* @param propertyPaths the projections to add.
*/
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder") // Projections available from getProjections
@NonNull
public SearchSpec.Builder addProjectionPathsForDocumentClass(
@@ -1034,6 +1091,7 @@
*/
// Individual parameters available from getResultGroupingTypeFlags and
// getResultGroupingLimit
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder setResultGrouping(@GroupingType int groupingTypeFlags, int limit) {
@@ -1076,6 +1134,7 @@
* @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)
@@ -1110,6 +1169,7 @@
* @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)
@@ -1152,6 +1212,7 @@
* @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)
@@ -1206,6 +1267,7 @@
* classpath
* @throws IllegalArgumentException if a weight is equal to or less than 0.0.
*/
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
@@ -1253,6 +1315,7 @@
* classpath
* @throws IllegalArgumentException if a weight is equal to or less than 0.0.
*/
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
@@ -1285,7 +1348,7 @@
// @exportToFramework:endStrip()
@NonNull
public Builder setNumericSearchEnabled(boolean enabled) {
- modifyEnabledFeature(Features.NUMERIC_SEARCH, enabled);
+ modifyEnabledFeature(FeatureConstants.NUMERIC_SEARCH, enabled);
return this;
}
@@ -1310,7 +1373,7 @@
// @exportToFramework:endStrip()
@NonNull
public Builder setVerbatimSearchEnabled(boolean enabled) {
- modifyEnabledFeature(Features.VERBATIM_SEARCH, enabled);
+ modifyEnabledFeature(FeatureConstants.VERBATIM_SEARCH, enabled);
return this;
}
@@ -1350,7 +1413,7 @@
// @exportToFramework:endStrip()
@NonNull
public Builder setListFilterQueryLanguageEnabled(boolean enabled) {
- modifyEnabledFeature(Features.LIST_FILTER_QUERY_LANGUAGE, enabled);
+ modifyEnabledFeature(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE, enabled);
return this;
}
@@ -1399,9 +1462,11 @@
bundle.putInt(RESULT_GROUPING_TYPE_FLAGS, mGroupingTypeFlags);
bundle.putInt(RESULT_GROUPING_LIMIT, mGroupingLimit);
if (!mTypePropertyWeights.isEmpty()
- && RANKING_STRATEGY_RELEVANCE_SCORE != mRankingStrategy) {
+ && RANKING_STRATEGY_RELEVANCE_SCORE != mRankingStrategy
+ && RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION != mRankingStrategy) {
throw new IllegalArgumentException("Property weights are only compatible with the "
- + "RANKING_STRATEGY_RELEVANCE_SCORE ranking strategy.");
+ + "RANKING_STRATEGY_RELEVANCE_SCORE and "
+ + "RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION ranking strategies.");
}
bundle.putBundle(TYPE_PROPERTY_WEIGHTS_FIELD, mTypePropertyWeights);
bundle.putString(ADVANCED_RANKING_EXPRESSION, mAdvancedRankingExpression);
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 2fea497..0007ece 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionResult.java
@@ -21,13 +21,14 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.util.BundleUtil;
import androidx.core.util.Preconditions;
/**
* The result class of the {@link AppSearchSession#searchSuggestionAsync}.
*/
-public class SearchSuggestionResult {
+public final class SearchSuggestionResult {
private static final String SUGGESTED_RESULT_FIELD = "suggestedResult";
private final Bundle mBundle;
@@ -92,6 +93,7 @@
*
* <p>The suggested result should only contain lowercase or special characters.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setSuggestedResult(@NonNull String suggestedResult) {
Preconditions.checkNotNull(suggestedResult);
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 b195591..26192fa 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java
@@ -15,7 +15,6 @@
*/
package androidx.appsearch.app;
-import static androidx.appsearch.app.AppSearchResult.RESULT_INVALID_ARGUMENT;
import android.annotation.SuppressLint;
import android.os.Bundle;
@@ -24,6 +23,7 @@
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.annotation.Document;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.util.BundleUtil;
@@ -45,9 +45,9 @@
* This class represents the specification logic for AppSearch. It can be used to set the filter
* and settings of search a suggestions.
*
- * @see AppSearchSession#searchSuggestionAsync(String, SearchSuggestionSpec)
+ * @see AppSearchSession#searchSuggestionAsync
*/
-public class SearchSuggestionSpec {
+public final class SearchSuggestionSpec {
static final String NAMESPACE_FIELD = "namespace";
static final String SCHEMA_FIELD = "schema";
static final String PROPERTY_FIELD = "property";
@@ -143,7 +143,8 @@
}
/** Returns the ranking strategy. */
- public @SuggestionRankingStrategy int getRankingStrategy() {
+ @SuggestionRankingStrategy
+ public int getRankingStrategy() {
return mBundle.getInt(RANKING_STRATEGY_FIELD);
}
@@ -222,7 +223,7 @@
private Bundle mTypePropertyFilters = new Bundle();
private Bundle mDocumentIds = new Bundle();
private final int mTotalResultCount;
- private @SuggestionRankingStrategy int mRankingStrategy =
+ @SuggestionRankingStrategy private int mRankingStrategy =
SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT;
private boolean mBuilt = false;
@@ -243,6 +244,7 @@
*
* <p>If unset, the query will search over all namespaces.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterNamespaces(@NonNull String... namespaces) {
Preconditions.checkNotNull(namespaces);
@@ -256,6 +258,7 @@
*
* <p>If unset, the query will search over all namespaces.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterNamespaces(@NonNull Collection<String> namespaces) {
Preconditions.checkNotNull(namespaces);
@@ -270,6 +273,7 @@
* <p>The default value {@link #SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT} will be used if
* this method is never called.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setRankingStrategy(@SuggestionRankingStrategy int rankingStrategy) {
Preconditions.checkArgumentInRange(rankingStrategy,
@@ -286,6 +290,7 @@
*
* <p>If unset, the query will search over all schema.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterSchemas(@NonNull String... schemaTypes) {
Preconditions.checkNotNull(schemaTypes);
@@ -299,6 +304,7 @@
*
* <p>If unset, the query will search over all schema.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterSchemas(@NonNull Collection<String> schemaTypes) {
Preconditions.checkNotNull(schemaTypes);
@@ -315,10 +321,12 @@
*
* <p>If unset, the query will search over all schema.
*
+ * <p>Merged list available from {@link #getFilterSchemas()}.
+ *
* @param documentClasses classes annotated with {@link Document}.
*/
- // Merged list available from getFilterSchemas()
@SuppressLint("MissingGetterMatchingBuilder")
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterDocumentClasses(@NonNull Class<?>... documentClasses)
throws AppSearchException {
@@ -337,10 +345,12 @@
*
* <p>If unset, the query will search over all schema.
*
+ * <p>Merged list available from {@link #getFilterSchemas()}.
+ *
* @param documentClasses classes annotated with {@link Document}.
*/
- // Merged list available from getFilterSchemas
@SuppressLint("MissingGetterMatchingBuilder")
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterDocumentClasses(
@NonNull Collection<? extends Class<?>> documentClasses) throws AppSearchException {
@@ -496,6 +506,7 @@
*
* <p>If unset, the query will search over all documents.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterDocumentIds(@NonNull String namespace,
@NonNull String... documentIds) {
@@ -511,6 +522,7 @@
*
* <p>If unset, the query will search over all documents.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterDocumentIds(@NonNull String namespace,
@NonNull Collection<String> documentIds) {
@@ -527,13 +539,13 @@
/** Constructs a new {@link SearchSpec} from the contents of this builder. */
@NonNull
- public SearchSuggestionSpec build() throws AppSearchException {
+ public SearchSuggestionSpec build() {
Bundle bundle = new Bundle();
if (!mSchemas.isEmpty()) {
Set<String> schemaFilter = new ArraySet<>(mSchemas);
for (String schema : mTypePropertyFilters.keySet()) {
if (!schemaFilter.contains(schema)) {
- throw new AppSearchException(RESULT_INVALID_ARGUMENT,
+ throw new IllegalStateException(
"The schema: " + schema + " exists in the property filter but "
+ "doesn't exist in the schema filter.");
}
@@ -543,7 +555,7 @@
Set<String> namespaceFilter = new ArraySet<>(mNamespaces);
for (String namespace : mDocumentIds.keySet()) {
if (!namespaceFilter.contains(namespace)) {
- throw new AppSearchException(RESULT_INVALID_ARGUMENT,
+ throw new IllegalStateException(
"The namespace: " + namespace + " exists in the document id "
+ "filter but doesn't exist in the namespace filter.");
}
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 52e953a..9b950ed 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
@@ -23,6 +23,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresFeature;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
@@ -76,7 +77,7 @@
* This deletes all documents that are incompatible with the new schema. The new schema is
* then saved and persisted to disk.
* <li>Add a {@link Migrator} for each incompatible type and make no deletion. The migrator
- * will migrate documents from it's old schema version to the new version. Migrated types
+ * will migrate documents from its old schema version to the new version. Migrated types
* will be set into both {@link SetSchemaResponse#getIncompatibleTypes()} and
* {@link SetSchemaResponse#getMigratedTypes()}. See the migration section below.
* </ul>
@@ -318,6 +319,7 @@
*
* <p>Any documents of these types will be displayed on system UI surfaces by default.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addSchemas(@NonNull AppSearchSchema... schemas) {
Preconditions.checkNotNull(schemas);
@@ -330,6 +332,7 @@
*
* <p>An {@link AppSearchSchema} object represents one type of structured data.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addSchemas(@NonNull Collection<AppSearchSchema> schemas) {
Preconditions.checkNotNull(schemas);
@@ -343,12 +346,15 @@
* Adds one or more {@link androidx.appsearch.annotation.Document} annotated classes to the
* schema.
*
+ * <p>Merged list available from {@link #getSchemas()}.
+ *
* @param documentClasses classes annotated with
* {@link androidx.appsearch.annotation.Document}.
* @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
* has not generated a schema for the given document classes.
*/
- @SuppressLint("MissingGetterMatchingBuilder") // Merged list available from getSchemas()
+ @CanIgnoreReturnValue
+ @SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder addDocumentClasses(@NonNull Class<?>... documentClasses)
throws AppSearchException {
@@ -361,12 +367,18 @@
* Adds a collection of {@link androidx.appsearch.annotation.Document} annotated classes to
* the schema.
*
+ * <p>This will also add all {@link androidx.appsearch.annotation.Document} classes
+ * referenced by the schema via document properties.
+ *
+ * <p>Merged list available from {@link #getSchemas()}.
+ *
* @param documentClasses classes annotated with
* {@link androidx.appsearch.annotation.Document}.
* @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
* has not generated a schema for the given document classes.
*/
- @SuppressLint("MissingGetterMatchingBuilder") // Merged list available from getSchemas()
+ @CanIgnoreReturnValue
+ @SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder addDocumentClasses(@NonNull Collection<? extends Class<?>> documentClasses)
throws AppSearchException {
@@ -377,6 +389,7 @@
for (Class<?> documentClass : documentClasses) {
DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
schemas.add(factory.getSchema());
+ addDocumentClasses(factory.getNestedDocumentClasses());
}
return addSchemas(schemas);
}
@@ -397,6 +410,7 @@
* @param displayed Whether documents of this type will be displayed on system UI surfaces.
*/
// Merged list available from getSchemasNotDisplayedBySystem
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder setSchemaTypeDisplayedBySystem(
@@ -438,6 +452,7 @@
* @throws IllegalArgumentException – if input unsupported permission.
*/
// Merged list available from getRequiredPermissionsForSchemaTypeVisibility
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
// @exportToFramework:startStrip()
@RequiresFeature(
@@ -465,6 +480,7 @@
/** 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)
@@ -498,6 +514,7 @@
* @param packageIdentifier Represents the package that will be granted visibility.
*/
// Merged list available from getSchemasVisibleToPackages
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder setSchemaTypeVisibilityForPackage(
@@ -553,6 +570,7 @@
* @see SetSchemaRequest.Builder#addSchemas
* @see AppSearchSession#setSchemaAsync
*/
+ @CanIgnoreReturnValue
@NonNull
@SuppressLint("MissingGetterMatchingBuilder") // Getter return plural objects.
public Builder setMigrator(@NonNull String schemaType, @NonNull Migrator migrator) {
@@ -580,7 +598,7 @@
* {@link Migrator#onUpgrade} or {@link Migrator#onDowngrade} must exist in this
* {@link SetSchemaRequest}.
*
- * @param migrators A {@link Map} of migrators that translate a document from it's current
+ * @param migrators A {@link Map} of migrators that translate a document from its current
* version to the final version set via {@link #setVersion}. The key of
* the map is the schema type that the {@link Migrator} value applies to.
*
@@ -588,6 +606,7 @@
* @see SetSchemaRequest.Builder#addSchemas
* @see AppSearchSession#setSchemaAsync
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setMigrators(@NonNull Map<String, Migrator> migrators) {
Preconditions.checkNotNull(migrators);
@@ -610,6 +629,8 @@
* <p>The default behavior, if this method is not called, is to allow types to be
* displayed on system UI surfaces.
*
+ * <p> Merged list available from {@link #getSchemasNotDisplayedBySystem()}.
+ *
* @param documentClass A class annotated with
* {@link androidx.appsearch.annotation.Document}, the visibility of
* which will be configured
@@ -618,7 +639,7 @@
* @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
* has not generated a schema for the given document class.
*/
- // Merged list available from getSchemasNotDisplayedBySystem
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder setDocumentClassDisplayedBySystem(@NonNull Class<?> documentClass,
@@ -647,6 +668,8 @@
*
* <p>By default, app data sharing between applications is disabled.
*
+ * <p>Merged list available from {@link #getSchemasVisibleToPackages()}.
+ *
* @param documentClass The {@link androidx.appsearch.annotation.Document} class to set
* visibility on.
* @param visible Whether the {@code documentClass} will be visible or not.
@@ -654,7 +677,7 @@
* @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
* has not generated a schema for the given document class.
*/
- // Merged list available from getSchemasVisibleToPackages
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder setDocumentClassVisibilityForPackage(@NonNull Class<?> documentClass,
@@ -682,6 +705,7 @@
* {@link #READ_CONTACTS}, {@link #READ_EXTERNAL_STORAGE},
* {@link #READ_HOME_APP_SEARCH_DATA} and {@link #READ_ASSISTANT_APP_SEARCH_DATA}.
*
+ * <p>Merged map available from {@link #getRequiredPermissionsForSchemaTypeVisibility()}.
* @see android.Manifest.permission#READ_SMS
* @see android.Manifest.permission#READ_CALENDAR
* @see android.Manifest.permission#READ_CONTACTS
@@ -695,7 +719,7 @@
* schema.
* @throws IllegalArgumentException – if input unsupported permission.
*/
- // Merged map available from getRequiredPermissionsForSchemaTypeVisibility
+ @CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
@@ -714,6 +738,7 @@
}
/** Clears all required permissions combinations for the given schema type. */
+ @CanIgnoreReturnValue
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
@@ -740,6 +765,7 @@
*
* <p>By default, this is {@code false}.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setForceOverride(boolean forceOverride) {
resetIfBuilt();
@@ -775,6 +801,7 @@
* @see Migrator
* @see SetSchemaRequest.Builder#setMigrator
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setVersion(@IntRange(from = 1) int version) {
Preconditions.checkArgument(version >= 1, "Version must be a positive number.");
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 3f8ce5e..ade162c 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
@@ -21,6 +21,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
@@ -182,6 +183,7 @@
private boolean mBuilt = false;
/** Adds {@link MigrationFailure}s to the list of migration failures. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addMigrationFailures(
@NonNull Collection<MigrationFailure> migrationFailures) {
@@ -192,6 +194,7 @@
}
/** Adds a {@link MigrationFailure} to the list of migration failures. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addMigrationFailure(@NonNull MigrationFailure migrationFailure) {
Preconditions.checkNotNull(migrationFailure);
@@ -201,6 +204,7 @@
}
/** Adds deletedTypes to the list of deleted schema types. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addDeletedTypes(@NonNull Collection<String> deletedTypes) {
Preconditions.checkNotNull(deletedTypes);
@@ -210,6 +214,7 @@
}
/** Adds one deletedType to the list of deleted schema types. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addDeletedType(@NonNull String deletedType) {
Preconditions.checkNotNull(deletedType);
@@ -219,6 +224,7 @@
}
/** Adds incompatibleTypes to the list of incompatible schema types. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addIncompatibleTypes(@NonNull Collection<String> incompatibleTypes) {
Preconditions.checkNotNull(incompatibleTypes);
@@ -228,6 +234,7 @@
}
/** Adds one incompatibleType to the list of incompatible schema types. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addIncompatibleType(@NonNull String incompatibleType) {
Preconditions.checkNotNull(incompatibleType);
@@ -237,6 +244,7 @@
}
/** Adds migratedTypes to the list of migrated schema types. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addMigratedTypes(@NonNull Collection<String> migratedTypes) {
Preconditions.checkNotNull(migratedTypes);
@@ -246,6 +254,7 @@
}
/** Adds one migratedType to the list of migrated schema types. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addMigratedType(@NonNull String migratedType) {
Preconditions.checkNotNull(migratedType);
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 0d7901d..5778bf8 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
@@ -20,6 +20,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.core.util.Preconditions;
/** The response class of {@code AppSearchSession#getStorageInfo}. */
@@ -78,6 +79,7 @@
private int mAliveNamespacesCount;
/** Sets the size in bytes. */
+ @CanIgnoreReturnValue
@NonNull
public StorageInfo.Builder setSizeBytes(long sizeBytes) {
mSizeBytes = sizeBytes;
@@ -85,6 +87,7 @@
}
/** Sets the number of alive documents. */
+ @CanIgnoreReturnValue
@NonNull
public StorageInfo.Builder setAliveDocumentsCount(int aliveDocumentsCount) {
mAliveDocumentsCount = aliveDocumentsCount;
@@ -92,6 +95,7 @@
}
/** Sets the number of alive namespaces. */
+ @CanIgnoreReturnValue
@NonNull
public StorageInfo.Builder setAliveNamespacesCount(int aliveNamespacesCount) {
mAliveNamespacesCount = aliveNamespacesCount;
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java
index fd79bd6..86053b0 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java
@@ -20,6 +20,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
@@ -162,6 +163,7 @@
}
/** Sets whether this schema has opted out of platform surfacing. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) {
return setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY,
@@ -169,6 +171,7 @@
}
/** Add {@link PackageIdentifier} of packages which has access to this schema. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addVisibleToPackages(@NonNull Set<PackageIdentifier> packageIdentifiers) {
Preconditions.checkNotNull(packageIdentifiers);
@@ -177,6 +180,7 @@
}
/** Add {@link PackageIdentifier} of packages which has access to this schema. */
+ @CanIgnoreReturnValue
@NonNull
public Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) {
Preconditions.checkNotNull(packageIdentifier);
@@ -191,6 +195,7 @@
* <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) {
Preconditions.checkNotNull(visibleToPermissions);
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java
index e859cb9..b8adf9a 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java
@@ -19,6 +19,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.collection.ArraySet;
import java.util.Set;
@@ -76,6 +77,7 @@
}
/** Sets whether this schema has opted out of platform surfacing. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setVisibleToAllRequiredPermissions(
@NonNull Set<Integer> allRequiredPermissions) {
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 98689f5..2930d2d 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
@@ -73,7 +73,8 @@
*
* @return One of the constants documented in {@link AppSearchResult#getResultCode}.
*/
- public @AppSearchResult.ResultCode int getResultCode() {
+ @AppSearchResult.ResultCode
+ public int getResultCode() {
return mResultCode;
}
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 6e3705e..5e13c59 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java
@@ -22,6 +22,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.annotation.Document;
import androidx.appsearch.app.DocumentClassFactory;
import androidx.appsearch.app.DocumentClassFactoryRegistry;
@@ -96,6 +97,7 @@
*
* <p>If unset, the observer will match documents of all types.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterSchemas(@NonNull String... schemas) {
Preconditions.checkNotNull(schemas);
@@ -109,6 +111,7 @@
*
* <p>If unset, the observer will match documents of all types.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterSchemas(@NonNull Collection<String> schemas) {
Preconditions.checkNotNull(schemas);
@@ -124,10 +127,12 @@
*
* <p>If unset, the observer will match documents of all types.
*
+ * <p>Merged list available from {@link #getFilterSchemas()}.
+ *
* @param documentClasses classes annotated with {@link Document}.
*/
- // Merged list available from getFilterSchemas()
@SuppressLint("MissingGetterMatchingBuilder")
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterDocumentClasses(@NonNull Class<?>... documentClasses)
throws AppSearchException {
@@ -142,10 +147,12 @@
*
* <p>If unset, the observer will match documents of all types.
*
+ * <p>Merged list available from {@link #getFilterSchemas()}.
+ *
* @param documentClasses classes annotated with {@link Document}.
*/
- // Merged list available from getFilterSchemas
@SuppressLint("MissingGetterMatchingBuilder")
+ @CanIgnoreReturnValue
@NonNull
public Builder addFilterDocumentClasses(
@NonNull Collection<? extends Class<?>> documentClasses) throws AppSearchException {
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 3cff1133..48d5b23 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/stats/SchemaMigrationStats.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/stats/SchemaMigrationStats.java
@@ -21,6 +21,7 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.app.AppSearchResult;
import androidx.appsearch.app.SetSchemaRequest;
import androidx.appsearch.util.BundleUtil;
@@ -220,6 +221,7 @@
}
/** Sets status code for the schema migration action. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
mBundle.putInt(STATUS_CODE_FIELD, statusCode);
@@ -227,6 +229,7 @@
}
/** Sets the latency for waiting the executor. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setExecutorAcquisitionLatencyMillis(int executorAcquisitionLatencyMillis) {
mBundle.putInt(EXECUTOR_ACQUISITION_MILLIS_FIELD, executorAcquisitionLatencyMillis);
@@ -235,6 +238,7 @@
/** Sets total latency for the schema migration action in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setTotalLatencyMillis(int totalLatencyMillis) {
mBundle.putInt(TOTAL_LATENCY_MILLIS_FIELD, totalLatencyMillis);
@@ -242,6 +246,7 @@
}
/** Sets latency for the GetSchema action in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setGetSchemaLatencyMillis(int getSchemaLatencyMillis) {
mBundle.putInt(GET_SCHEMA_LATENCY_MILLIS_FIELD, getSchemaLatencyMillis);
@@ -252,6 +257,7 @@
* Sets latency for querying all documents that need to be migrated to new version and
* transforming documents to new version in milliseconds.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setQueryAndTransformLatencyMillis(
int queryAndTransformLatencyMillis) {
@@ -261,6 +267,7 @@
}
/** Sets latency of first SetSchema action in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setFirstSetSchemaLatencyMillis(
int firstSetSchemaLatencyMillis) {
@@ -269,6 +276,7 @@
}
/** Returns status of the first SetSchema action. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setIsFirstSetSchemaSuccess(boolean isFirstSetSchemaSuccess) {
mBundle.putBoolean(IS_FIRST_SET_SCHEMA_SUCCESS_FIELD, isFirstSetSchemaSuccess);
@@ -276,6 +284,7 @@
}
/** Sets latency of second SetSchema action in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setSecondSetSchemaLatencyMillis(
int secondSetSchemaLatencyMillis) {
@@ -284,6 +293,7 @@
}
/** Sets latency for putting migrated document to Icing lib in milliseconds. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setSaveDocumentLatencyMillis(
int saveDocumentLatencyMillis) {
@@ -292,6 +302,7 @@
}
/** Sets number of document that need to be migrated to another version. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setTotalNeedMigratedDocumentCount(int migratedDocumentCount) {
mBundle.putInt(TOTAL_NEED_MIGRATED_DOCUMENT_COUNT_FIELD, migratedDocumentCount);
@@ -299,6 +310,7 @@
}
/** Sets total document count of successfully migrated and saved in Icing. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setTotalSuccessMigratedDocumentCount(
int totalSuccessMigratedDocumentCount) {
@@ -308,6 +320,7 @@
}
/** Sets number of {@link androidx.appsearch.app.SetSchemaResponse.MigrationFailure}. */
+ @CanIgnoreReturnValue
@NonNull
public Builder setMigrationFailureCount(int migrationFailureCount) {
mBundle.putInt(MIGRATION_FAILURE_COUNT_FIELD, migrationFailureCount);
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/IndentingStringBuilder.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/IndentingStringBuilder.java
index ea5717e..20ef8fa 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/util/IndentingStringBuilder.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/IndentingStringBuilder.java
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
/**
* Utility for building indented strings.
@@ -41,6 +42,7 @@
/**
* Increases the indent level by one for appended strings.
*/
+ @CanIgnoreReturnValue
@NonNull
public IndentingStringBuilder increaseIndentLevel() {
mIndentLevel++;
@@ -50,6 +52,7 @@
/**
* Decreases the indent level by one for appended strings.
*/
+ @CanIgnoreReturnValue
@NonNull
public IndentingStringBuilder decreaseIndentLevel() throws IllegalStateException {
if (mIndentLevel == 0) {
@@ -64,6 +67,7 @@
*
* <p>Indentation is applied after each newline character.
*/
+ @CanIgnoreReturnValue
@NonNull
public IndentingStringBuilder append(@NonNull String str) {
applyIndentToString(str);
@@ -76,6 +80,7 @@
*
* <p>Indentation is applied after each newline character.
*/
+ @CanIgnoreReturnValue
@NonNull
public IndentingStringBuilder append(@NonNull Object obj) {
applyIndentToString(obj.toString());
diff --git a/appsearch/compiler/build.gradle b/appsearch/compiler/build.gradle
index f2c72b7..cb9caf4 100644
--- a/appsearch/compiler/build.gradle
+++ b/appsearch/compiler/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = 'AndroidX AppSearch Compiler'
+ name = "AppSearch Compiler"
type = LibraryType.ANNOTATION_PROCESSOR
inceptionYear = '2019'
description = 'Compiler for classes annotated with @androidx.appsearch.annotation.Document'
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 a80f718..5adef09 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
@@ -22,11 +22,15 @@
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
+import com.squareup.javapoet.WildcardTypeName;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
@@ -43,6 +47,7 @@
private final ProcessingEnvironment mEnv;
private final IntrospectionHelper mHelper;
private final DocumentModel mModel;
+ private final Set<ClassName> mDocumentTypesAccumulator = new HashSet<>();
public static void generate(
@NonNull ProcessingEnvironment env,
@@ -73,17 +78,55 @@
.addStatement("return SCHEMA_NAME")
.build());
+ CodeBlock schemaInitializer = createSchemaInitializerGetDocumentTypes();
+
classBuilder.addMethod(
MethodSpec.methodBuilder("getSchema")
.addModifiers(Modifier.PUBLIC)
.returns(mHelper.getAppSearchClass("AppSearchSchema"))
.addAnnotation(Override.class)
.addException(mHelper.getAppSearchExceptionClass())
- .addStatement("return $L", createSchemaInitializer())
+ .addStatement("return $L", schemaInitializer)
.build());
+
+ classBuilder.addMethod(createNestedClassesMethod());
}
- private CodeBlock createSchemaInitializer() throws ProcessingException {
+ @NonNull
+ private MethodSpec createNestedClassesMethod() {
+ TypeName setOfClasses = ParameterizedTypeName.get(ClassName.get("java.util", "List"),
+ ParameterizedTypeName.get(ClassName.get(Class.class),
+ WildcardTypeName.subtypeOf(Object.class)));
+
+ TypeName arraySetOfClasses =
+ ParameterizedTypeName.get(ClassName.get("java.util", "ArrayList"),
+ ParameterizedTypeName.get(ClassName.get(Class.class),
+ WildcardTypeName.subtypeOf(Object.class)));
+
+ MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("getNestedDocumentClasses")
+ .addModifiers(Modifier.PUBLIC)
+ .returns(setOfClasses)
+ .addAnnotation(Override.class)
+ .addException(mHelper.getAppSearchExceptionClass());
+
+ if (mDocumentTypesAccumulator.isEmpty()) {
+ methodBuilder.addStatement("return $T.emptyList()",
+ ClassName.get("java.util", "Collections"));
+ } else {
+ methodBuilder.addStatement("$T classSet = new $T()", setOfClasses, arraySetOfClasses);
+ for (ClassName className : mDocumentTypesAccumulator) {
+ methodBuilder.addStatement("classSet.add($T.class)", className);
+ }
+ methodBuilder.addStatement("return classSet").build();
+ }
+ return methodBuilder.build();
+ }
+
+ /**
+ * This method accumulates Document-type properties in mDocumentTypesAccumulator by calling
+ * {@link #createPropertySchema}.
+ */
+ private CodeBlock createSchemaInitializerGetDocumentTypes() throws ProcessingException {
CodeBlock.Builder codeBlock = CodeBlock.builder()
.add("new $T(SCHEMA_NAME)", mHelper.getAppSearchClass("AppSearchSchema", "Builder"))
.indent();
@@ -94,6 +137,7 @@
return codeBlock.build();
}
+ /** This method accumulates Document-type properties in mDocumentTypesAccumulator. */
private CodeBlock createPropertySchema(@NonNull VariableElement property)
throws ProcessingException {
AnnotationMirror annotation = mModel.getPropertyAnnotation(property);
@@ -166,6 +210,7 @@
propertyClass.nestedClass("Builder"),
propertyName,
documentFactoryClass);
+ mDocumentTypesAccumulator.add(documentClass);
} else {
codeBlock.add("new $T($S)", propertyClass.nestedClass("Builder"), propertyName);
}
@@ -227,6 +272,28 @@
}
codeBlock.add("\n.setIndexingType($T)", indexingEnum);
+ int joinableValueType = Integer.parseInt(params.get("joinableValueType").toString());
+ ClassName joinableEnum;
+ if (joinableValueType == 0) { // JOINABLE_VALUE_TYPE_NONE
+ joinableEnum = mHelper.getAppSearchClass(
+ "AppSearchSchema", "StringPropertyConfig", "JOINABLE_VALUE_TYPE_NONE");
+
+ } else if (joinableValueType == 1) { // JOINABLE_VALUE_TYPE_QUALIFIED_ID
+ if (repeated) {
+ throw new ProcessingException(
+ "Joinable value type " + joinableValueType + " not allowed on repeated "
+ + "properties.", property);
+
+ }
+ joinableEnum = mHelper.getAppSearchClass(
+ "AppSearchSchema", "StringPropertyConfig",
+ "JOINABLE_VALUE_TYPE_QUALIFIED_ID");
+ } else {
+ throw new ProcessingException(
+ "Unknown joinable value type " + joinableValueType, property);
+ }
+ codeBlock.add("\n.setJoinableValueType($T)", joinableEnum);
+
} else if (isPropertyDocument) {
if (params.containsKey("indexNestedProperties")) {
boolean indexNestedProperties = Boolean.parseBoolean(
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 b36c5877..43e24214 100644
--- a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
+++ b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
@@ -1190,6 +1190,38 @@
}
@Test
+ public void testStringPropertyJoinableType() throws Exception {
+ Compilation compilation = compile(
+ "import java.util.*;\n"
+ + "@Document\n"
+ + "public class Gift {\n"
+ + " @Document.Namespace String namespace;\n"
+ + " @Document.Id String id;\n"
+ + " @Document.StringProperty(joinableValueType=1)\n"
+ + " String object;\n"
+ + "}\n");
+
+ assertThat(compilation).succeededWithoutWarnings();
+ checkEqualsGolden("Gift.java");
+ }
+
+ @Test
+ public void testRepeatedPropertyJoinableType_throwsError() throws Exception {
+ Compilation compilation = compile(
+ "import java.util.*;\n"
+ + "@Document\n"
+ + "public class Gift {\n"
+ + " @Document.Namespace String namespace;\n"
+ + " @Document.Id String id;\n"
+ + " @Document.StringProperty(joinableValueType=1)\n"
+ + " List<String> object;\n"
+ + "}\n");
+
+ assertThat(compilation).hadErrorContaining(
+ "Joinable value type 1 not allowed on repeated properties.");
+ }
+
+ @Test
public void testPropertyName() throws Exception {
Compilation compilation = compile(
"import java.util.*;\n"
@@ -1504,6 +1536,44 @@
checkEqualsGolden("Gift.java");
}
+ @Test
+ public void testMultipleNesting() throws Exception {
+ Compilation compilation = compile(
+ "import java.util.*;\n"
+ + "@Document\n"
+ + "public class Gift {\n"
+ + " @Document.Id String id;\n"
+ + " @Document.Namespace String namespace;\n"
+ + " @Document.DocumentProperty Middle middleContentA;\n"
+ + " @Document.DocumentProperty Middle middleContentB;\n"
+ + "}\n"
+ + "\n"
+ + "@Document\n"
+ + "class Middle {\n"
+ + " @Document.Id String id;\n"
+ + " @Document.Namespace String namespace;\n"
+ + " @Document.DocumentProperty Inner innerContentA;\n"
+ + " @Document.DocumentProperty Inner innerContentB;\n"
+ + "}\n"
+ + "@Document\n"
+ + "class Inner {\n"
+ + " @Document.Id String id;\n"
+ + " @Document.Namespace String namespace;\n"
+ + " @Document.StringProperty String contents;\n"
+ + "}\n");
+
+ assertThat(compilation).succeededWithoutWarnings();
+ checkEqualsGolden("Gift.java");
+
+ // Check that Gift contains Middle, Middle contains Inner, and Inner returns empty
+ checkResultContains(/* className= */ "Gift.java",
+ /* content= */ "classSet.add(Middle.class);\n return classSet;");
+ checkResultContains(/* className= */ "Middle.java",
+ /* content= */ "classSet.add(Inner.class);\n return classSet;");
+ checkResultContains(/* className= */ "Inner.java",
+ /* content= */ "return Collections.emptyList();");
+ }
+
private Compilation compile(String classBody) {
return compile("Gift", classBody);
}
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 094d178..a49931b 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
@@ -5,12 +5,15 @@
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.exceptions.AppSearchException;
import java.lang.Boolean;
+import java.lang.Class;
import java.lang.Double;
import java.lang.Float;
import java.lang.Integer;
import java.lang.Long;
import java.lang.Override;
import java.lang.String;
+import java.util.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -29,6 +32,7 @@
.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.LongPropertyConfig.Builder("integerProp")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
@@ -54,6 +58,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_field.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_field.JAVA
index 36930bc1..3f9ad76 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_field.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_field.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -28,6 +31,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA
index 35fdea2..1d97d08 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -28,6 +31,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() throws AppSearchException {
+ return Collections.emptyList();
+ }
+
+ @Override
public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
GenericDocument.Builder<?> builder =
new GenericDocument.Builder<>(document.namespace, document.getId(), SCHEMA_NAME);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocument.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocument.JAVA
index ab783ed..fc27579 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocument.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocument.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -24,11 +27,17 @@
.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())
.build();
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA
index 0f275a7..d7954e1 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA
@@ -4,10 +4,12 @@
import androidx.appsearch.app.DocumentClassFactory;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.exceptions.AppSearchException;
+import java.lang.Class;
import java.lang.Float;
import java.lang.Override;
import java.lang.String;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import javax.annotation.processing.Generated;
@@ -27,11 +29,13 @@
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
.build())
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("repeatNoReq")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
.build())
.addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("req")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
@@ -43,6 +47,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDifferentTypeName.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDifferentTypeName.JAVA
index 7a7bca6..eb8b33b 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDifferentTypeName.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDifferentTypeName.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -24,6 +27,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testGetterAndSetterFunctions_withFieldName.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testGetterAndSetterFunctions_withFieldName.JAVA
index aa566b3..3096bdc 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testGetterAndSetterFunctions_withFieldName.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testGetterAndSetterFunctions_withFieldName.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -28,6 +31,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA
index 810a16f..acad761 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -24,21 +27,29 @@
.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.StringPropertyConfig.Builder("indexExact")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
.build())
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("indexPrefix")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
.build())
.build();
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA
index ac333bc..d55520d 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -24,11 +27,17 @@
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
.build())
.build();
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() throws AppSearchException {
+ return Collections.emptyList();
+ }
+
+ @Override
public GenericDocument toGenericDocument(Gift.InnerGift document) throws AppSearchException {
GenericDocument.Builder<?> builder =
new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testLongPropertyIndexingType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testLongPropertyIndexingType.JAVA
index c754aef..abe3acb 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testLongPropertyIndexingType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testLongPropertyIndexingType.JAVA
@@ -4,10 +4,13 @@
import androidx.appsearch.app.DocumentClassFactory;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.exceptions.AppSearchException;
+import java.lang.Class;
import java.lang.Integer;
import java.lang.Long;
import java.lang.Override;
import java.lang.String;
+import java.util.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -66,6 +69,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testMultipleNestedAutoValueDocument.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testMultipleNestedAutoValueDocument.JAVA
index 31c563e..856821f 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testMultipleNestedAutoValueDocument.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testMultipleNestedAutoValueDocument.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -24,6 +27,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() throws AppSearchException {
+ return Collections.emptyList();
+ }
+
+ @Override
public GenericDocument toGenericDocument(Gift.A document) throws AppSearchException {
GenericDocument.Builder<?> builder =
new GenericDocument.Builder<>(document.namespace(), document.id(), SCHEMA_NAME);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testMultipleNesting.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testMultipleNesting.JAVA
new file mode 100644
index 0000000..7d8acd0
--- /dev/null
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testMultipleNesting.JAVA
@@ -0,0 +1,82 @@
+package com.example.appsearch;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.DocumentClassFactory;
+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.ArrayList;
+import java.util.List;
+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.DocumentPropertyConfig.Builder("middleContentA", $$__AppSearch__Middle.SCHEMA_NAME)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setShouldIndexNestedProperties(false)
+ .build())
+ .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder("middleContentB", $$__AppSearch__Middle.SCHEMA_NAME)
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setShouldIndexNestedProperties(false)
+ .build())
+ .build();
+ }
+
+ @Override
+ public List<Class<?>> getNestedDocumentClasses() throws AppSearchException {
+ List<Class<?>> classSet = new ArrayList<Class<?>>();
+ classSet.add(Middle.class);
+ return classSet;
+ }
+
+ @Override
+ public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
+ GenericDocument.Builder<?> builder =
+ new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+ Middle middleContentACopy = document.middleContentA;
+ if (middleContentACopy != null) {
+ GenericDocument middleContentAConv = GenericDocument.fromDocumentClass(middleContentACopy);
+ builder.setPropertyDocument("middleContentA", middleContentAConv);
+ }
+ Middle middleContentBCopy = document.middleContentB;
+ if (middleContentBCopy != null) {
+ GenericDocument middleContentBConv = GenericDocument.fromDocumentClass(middleContentBCopy);
+ builder.setPropertyDocument("middleContentB", middleContentBConv);
+ }
+ return builder.build();
+ }
+
+ @Override
+ public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
+ String idConv = genericDoc.getId();
+ String namespaceConv = genericDoc.getNamespace();
+ GenericDocument middleContentACopy = genericDoc.getPropertyDocument("middleContentA");
+ Middle middleContentAConv = null;
+ if (middleContentACopy != null) {
+ middleContentAConv = middleContentACopy.toDocumentClass(Middle.class);
+ }
+ GenericDocument middleContentBCopy = genericDoc.getPropertyDocument("middleContentB");
+ Middle middleContentBConv = null;
+ if (middleContentBCopy != null) {
+ middleContentBConv = middleContentBCopy.toDocumentClass(Middle.class);
+ }
+ Gift document = new Gift();
+ document.id = idConv;
+ document.namespace = namespaceConv;
+ document.middleContentA = middleContentAConv;
+ document.middleContentB = middleContentBConv;
+ return document;
+ }
+}
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testNestedDocumentsIndexing.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testNestedDocumentsIndexing.JAVA
index c31f620..b24a19b 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testNestedDocumentsIndexing.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testNestedDocumentsIndexing.JAVA
@@ -4,6 +4,7 @@
import androidx.appsearch.app.DocumentClassFactory;
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.ArrayList;
@@ -51,6 +52,13 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() throws AppSearchException {
+ List<Class<?>> classSet = new ArrayList<Class<?>>();
+ classSet.add(GiftContent.class);
+ return classSet;
+ }
+
+ @Override
public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
GenericDocument.Builder<?> builder =
new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testOneBadConstructor.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testOneBadConstructor.JAVA
index f0d8cb5..f3cca50 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testOneBadConstructor.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testOneBadConstructor.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -24,6 +27,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() throws AppSearchException {
+ return Collections.emptyList();
+ }
+
+ @Override
public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
GenericDocument.Builder<?> builder =
new GenericDocument.Builder<>(document.getNamespace(), document.getId(), SCHEMA_NAME);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA
index f6672c0..2a730d5 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -24,11 +27,17 @@
.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())
.build();
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA
index f07c03b..44d3db6 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -28,6 +31,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_isGetterForBoolean.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_isGetterForBoolean.JAVA
index 9e828fd..a676f5c 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_isGetterForBoolean.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_isGetterForBoolean.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -27,6 +30,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA
index f46d524..89b7dbb 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA
@@ -4,12 +4,14 @@
import androidx.appsearch.app.DocumentClassFactory;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.exceptions.AppSearchException;
+import java.lang.Class;
import java.lang.Integer;
import java.lang.Override;
import java.lang.String;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import javax.annotation.processing.Generated;
@@ -29,6 +31,7 @@
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
.build())
.addProperty(new AppSearchSchema.LongPropertyConfig.Builder("setOfInt")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
@@ -44,6 +47,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testStringPropertyJoinableType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testStringPropertyJoinableType.JAVA
new file mode 100644
index 0000000..722ebf59
--- /dev/null
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testStringPropertyJoinableType.JAVA
@@ -0,0 +1,66 @@
+package com.example.appsearch;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.DocumentClassFactory;
+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.Collections;
+import java.util.List;
+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("object")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+ .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+ .build())
+ .build();
+ }
+
+ @Override
+ public List<Class<?>> getNestedDocumentClasses() 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 objectCopy = document.object;
+ if (objectCopy != null) {
+ builder.setPropertyString("object", objectCopy);
+ }
+ return builder.build();
+ }
+
+ @Override
+ public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
+ String idConv = genericDoc.getId();
+ String namespaceConv = genericDoc.getNamespace();
+ String[] objectCopy = genericDoc.getPropertyStringArray("object");
+ String objectConv = null;
+ if (objectCopy != null && objectCopy.length != 0) {
+ objectConv = objectCopy[0];
+ }
+ Gift document = new Gift();
+ document.namespace = namespaceConv;
+ document.id = idConv;
+ document.object = objectConv;
+ return document;
+ }
+}
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA
index de3177f..c30c660 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -34,6 +37,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass.JAVA
index ef1410d..9694ee2 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -24,11 +27,13 @@
.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.StringPropertyConfig.Builder("sender")
.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.BooleanPropertyConfig.Builder("foo")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
@@ -37,6 +42,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() throws AppSearchException {
+ return Collections.emptyList();
+ }
+
+ @Override
public GenericDocument toGenericDocument(Gift.FooGift document) throws AppSearchException {
GenericDocument.Builder<?> builder =
new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClassPojoAncestor.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClassPojoAncestor.JAVA
index f51fa57..8f600f6 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClassPojoAncestor.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClassPojoAncestor.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -24,11 +27,13 @@
.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.StringPropertyConfig.Builder("sender")
.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.BooleanPropertyConfig.Builder("foo")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
@@ -37,6 +42,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() throws AppSearchException {
+ return Collections.emptyList();
+ }
+
+ @Override
public GenericDocument toGenericDocument(Gift.FooGift document) throws AppSearchException {
GenericDocument.Builder<?> builder =
new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClassWithPrivateFields.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClassWithPrivateFields.JAVA
index 3fed9e3..d7feadb 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClassWithPrivateFields.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClassWithPrivateFields.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -24,21 +27,29 @@
.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.StringPropertyConfig.Builder("receiver")
.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.StringPropertyConfig.Builder("sender")
.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())
.build();
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() throws AppSearchException {
+ return Collections.emptyList();
+ }
+
+ @Override
public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
GenericDocument.Builder<?> builder =
new GenericDocument.Builder<>(document.getNamespace(), document.getId(), SCHEMA_NAME);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass_changeSchemaName.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass_changeSchemaName.JAVA
index 812463a..08e306c 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass_changeSchemaName.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass_changeSchemaName.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -24,16 +27,23 @@
.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.StringPropertyConfig.Builder("sender")
.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())
.build();
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass_multipleChangedSchemaNames.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass_multipleChangedSchemaNames.JAVA
index b77c03b..ac5dcb2 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass_multipleChangedSchemaNames.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuperClass_multipleChangedSchemaNames.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -24,11 +27,13 @@
.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.StringPropertyConfig.Builder("sender")
.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.BooleanPropertyConfig.Builder("foo")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
@@ -37,6 +42,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() throws AppSearchException {
+ return Collections.emptyList();
+ }
+
+ @Override
public GenericDocument toGenericDocument(FooGift document) throws AppSearchException {
GenericDocument.Builder<?> builder =
new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
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 d7e9290..0643cf9 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
@@ -6,6 +6,7 @@
import androidx.appsearch.exceptions.AppSearchException;
import java.lang.Boolean;
import java.lang.Byte;
+import java.lang.Class;
import java.lang.Double;
import java.lang.Float;
import java.lang.Integer;
@@ -54,6 +55,7 @@
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
.build())
.addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder("collectGift", $$__AppSearch__Gift.SCHEMA_NAME)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
@@ -103,6 +105,7 @@
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
.build())
.addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder("arrGift", $$__AppSearch__Gift.SCHEMA_NAME)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
@@ -112,6 +115,7 @@
.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.LongPropertyConfig.Builder("boxLong")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
@@ -158,6 +162,13 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() throws AppSearchException {
+ List<Class<?>> classSet = new ArrayList<Class<?>>();
+ classSet.add(Gift.class);
+ return classSet;
+ }
+
+ @Override
public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
GenericDocument.Builder<?> builder =
new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
index 678c212..15e72e7 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -24,66 +27,83 @@
.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.StringPropertyConfig.Builder("tokPlainInvalid")
.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.StringPropertyConfig.Builder("tokVerbatimInvalid")
.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.StringPropertyConfig.Builder("tokRfc822Invalid")
.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.StringPropertyConfig.Builder("tokNone")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
.build())
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokPlain")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
.build())
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokVerbatim")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_VERBATIM)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
.build())
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokRfc822")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_RFC822)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
.build())
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokNonePrefix")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
.build())
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokPlainPrefix")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
.build())
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokVerbatimPrefix")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_VERBATIM)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
.build())
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokRfc822Prefix")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_RFC822)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
.build())
.build();
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleConventions.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleConventions.JAVA
index b7f69fc..cea4e5d 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleConventions.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleConventions.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -40,6 +43,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() throws AppSearchException {
+ return Collections.emptyList();
+ }
+
+ @Override
public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
GenericDocument.Builder<?> builder =
new GenericDocument.Builder<>(document.namespace, document.getId(), SCHEMA_NAME);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleSetters.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleSetters.JAVA
index f07c03b..44d3db6 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleSetters.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleSetters.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -28,6 +31,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_usableFactoryMethod_unusableConstructor.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_usableFactoryMethod_unusableConstructor.JAVA
index 34c2b72..e356243 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_usableFactoryMethod_unusableConstructor.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_usableFactoryMethod_unusableConstructor.JAVA
@@ -4,8 +4,11 @@
import androidx.appsearch.app.DocumentClassFactory;
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.Collections;
+import java.util.List;
import javax.annotation.processing.Generated;
@Generated("androidx.appsearch.compiler.AppSearchCompiler")
@@ -28,6 +31,11 @@
}
@Override
+ public List<Class<?>> getNestedDocumentClasses() 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);
diff --git a/arch/core/core-common/build.gradle b/arch/core/core-common/build.gradle
index 8a60844..f8d646f7 100644
--- a/arch/core/core-common/build.gradle
+++ b/arch/core/core-common/build.gradle
@@ -29,7 +29,7 @@
}
androidx {
- name = "Android Arch-Common"
+ name = "Arch-Common"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Arch-Common"
diff --git a/arch/core/core-runtime/build.gradle b/arch/core/core-runtime/build.gradle
index 6652bdf..f1d60e1 100644
--- a/arch/core/core-runtime/build.gradle
+++ b/arch/core/core-runtime/build.gradle
@@ -27,7 +27,7 @@
}
androidx {
- name = "Android Arch-Runtime"
+ name = "Arch-Runtime"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Arch-Runtime"
diff --git a/arch/core/core-testing/build.gradle b/arch/core/core-testing/build.gradle
index 0476e53..4b071ea 100644
--- a/arch/core/core-testing/build.gradle
+++ b/arch/core/core-testing/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "Android Core-Testing"
+ name = "Core-Testing"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Core-Testing"
diff --git a/asynclayoutinflater/asynclayoutinflater-appcompat/build.gradle b/asynclayoutinflater/asynclayoutinflater-appcompat/build.gradle
index 8f08e8c..a6034b9 100644
--- a/asynclayoutinflater/asynclayoutinflater-appcompat/build.gradle
+++ b/asynclayoutinflater/asynclayoutinflater-appcompat/build.gradle
@@ -13,10 +13,11 @@
}
androidx {
- name = "AsyncLayoutInflater integration for AppCompat"
+ name = "AsyncLayoutInflater AppCompat"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2022"
- description = "A thread-safe LayoutInflater Factory that provides compatibility between AsyncLayoutInflater and AppCompat."
+ description = "A thread-safe LayoutInflater Factory that provides compatibility between " +
+ "AsyncLayoutInflater and AppCompat."
}
android {
diff --git a/asynclayoutinflater/asynclayoutinflater/build.gradle b/asynclayoutinflater/asynclayoutinflater/build.gradle
index 713ec8e..44ec7c0 100644
--- a/asynclayoutinflater/asynclayoutinflater/build.gradle
+++ b/asynclayoutinflater/asynclayoutinflater/build.gradle
@@ -19,10 +19,10 @@
}
androidx {
- name = "Android Support Library Async Layout Inflater"
+ name = "AsyncLayoutInflater"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
- description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
+ description = "Provides support for inflating layouts off the UI thread."
}
android {
diff --git a/autofill/autofill/build.gradle b/autofill/autofill/build.gradle
index c4a5853..e5bbb65 100644
--- a/autofill/autofill/build.gradle
+++ b/autofill/autofill/build.gradle
@@ -34,7 +34,7 @@
}
androidx {
- name = "AndroidX Autofill"
+ name = "Autofill"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
description = "AndroidX Autofill"
diff --git a/benchmark/baseline-profile-gradle-plugin/build.gradle b/benchmark/baseline-profile-gradle-plugin/build.gradle
index 9038336..e4884b6 100644
--- a/benchmark/baseline-profile-gradle-plugin/build.gradle
+++ b/benchmark/baseline-profile-gradle-plugin/build.gradle
@@ -72,7 +72,7 @@
}
androidx {
- name = "Android Baseline Profile Gradle Plugin"
+ name = "Baseline Profile Gradle Plugin"
publish = Publish.SNAPSHOT_AND_RELEASE
type = LibraryType.GRADLE_PLUGIN
inceptionYear = "2022"
diff --git a/benchmark/baseline-profile-gradle-plugin/src/test/resources/robolectric.properties b/benchmark/baseline-profile-gradle-plugin/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..69fde47
--- /dev/null
+++ b/benchmark/baseline-profile-gradle-plugin/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/benchmark/benchmark-common/build.gradle b/benchmark/benchmark-common/build.gradle
index e8f2eca..b66a11b 100644
--- a/benchmark/benchmark-common/build.gradle
+++ b/benchmark/benchmark-common/build.gradle
@@ -66,7 +66,7 @@
}
androidx {
- name = "Android Benchmark - Common"
+ name = "Benchmark - Common"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Android Benchmark - Common"
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/IsolationActivity.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/IsolationActivity.kt
index 475d34f..0f716e2 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/IsolationActivity.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/IsolationActivity.kt
@@ -53,6 +53,7 @@
setContentView(R.layout.isolation_activity)
// disable launch animation
+ @Suppress("Deprecation")
overridePendingTransition(0, 0)
if (firstInit) {
@@ -117,6 +118,7 @@
public fun actuallyFinish() {
// disable close animation
+ @Suppress("Deprecation")
overridePendingTransition(0, 0)
super.finish()
}
diff --git a/benchmark/benchmark-darwin-core/build.gradle b/benchmark/benchmark-darwin-core/build.gradle
index ac9243d..12b8a6b 100644
--- a/benchmark/benchmark-darwin-core/build.gradle
+++ b/benchmark/benchmark-darwin-core/build.gradle
@@ -39,7 +39,7 @@
}
androidx {
- name = "AndroidX Benchmarks - Darwin Core"
+ name = "Benchmarks - Darwin Core"
inceptionYear = "2022"
description = "AndroidX Benchmarks - Darwin Core"
}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/build.gradle b/benchmark/benchmark-darwin-gradle-plugin/build.gradle
index 7a16cf9..5c3aa31 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/build.gradle
+++ b/benchmark/benchmark-darwin-gradle-plugin/build.gradle
@@ -44,7 +44,7 @@
}
androidx {
- name = "AndroidX Benchmarks - Darwin Gradle Plugin"
+ name = "Benchmarks - Darwin Gradle Plugin"
publish = Publish.SNAPSHOT_ONLY
type = LibraryType.GRADLE_PLUGIN
inceptionYear = "2022"
diff --git a/benchmark/benchmark-darwin-samples/build.gradle b/benchmark/benchmark-darwin-samples/build.gradle
index c297722..3684119 100644
--- a/benchmark/benchmark-darwin-samples/build.gradle
+++ b/benchmark/benchmark-darwin-samples/build.gradle
@@ -60,7 +60,7 @@
}
androidx {
- name = "AndroidX Benchmarks - Darwin Samples"
+ name = "Benchmarks - Darwin Samples"
inceptionYear = "2022"
description = "AndroidX Benchmarks - Darwin Samples"
}
diff --git a/benchmark/benchmark-darwin/build.gradle b/benchmark/benchmark-darwin/build.gradle
index 473a5c3..e192b8f 100644
--- a/benchmark/benchmark-darwin/build.gradle
+++ b/benchmark/benchmark-darwin/build.gradle
@@ -43,7 +43,7 @@
}
androidx {
- name = "AndroidX Benchmarks - Darwin"
+ name = "Benchmarks - Darwin"
inceptionYear = "2022"
description = "AndroidX Benchmarks - Darwin"
}
diff --git a/benchmark/benchmark-junit4/build.gradle b/benchmark/benchmark-junit4/build.gradle
index 4db5a73..cfb08c2 100644
--- a/benchmark/benchmark-junit4/build.gradle
+++ b/benchmark/benchmark-junit4/build.gradle
@@ -47,7 +47,7 @@
}
androidx {
- name = "Android Benchmark - JUnit4"
+ name = "Benchmark - JUnit4"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
description = "Android Benchmark - JUnit4"
diff --git a/benchmark/benchmark-macro-junit4/build.gradle b/benchmark/benchmark-macro-junit4/build.gradle
index cd4804f..5fa8211 100644
--- a/benchmark/benchmark-macro-junit4/build.gradle
+++ b/benchmark/benchmark-macro-junit4/build.gradle
@@ -53,7 +53,7 @@
}
androidx {
- name = "Android Benchmark - Macrobenchmark JUnit4"
+ name = "Benchmark - Macrobenchmark JUnit4"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2020"
description = "Android Benchmark - Macrobenchmark JUnit4"
diff --git a/benchmark/benchmark-macro/build.gradle b/benchmark/benchmark-macro/build.gradle
index 538d68d..bc6c8e3 100644
--- a/benchmark/benchmark-macro/build.gradle
+++ b/benchmark/benchmark-macro/build.gradle
@@ -80,7 +80,7 @@
}
androidx {
- name = "Android Benchmark - Macrobenchmark"
+ name = "Benchmark - Macrobenchmark"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2020"
description = "Android Benchmark - Macrobenchmark"
diff --git a/benchmark/gradle-plugin/build.gradle b/benchmark/gradle-plugin/build.gradle
index fdf95af..11bf133 100644
--- a/benchmark/gradle-plugin/build.gradle
+++ b/benchmark/gradle-plugin/build.gradle
@@ -58,7 +58,7 @@
}
androidx {
- name = "Android Benchmark Gradle Plugin"
+ name = "Benchmark Gradle Plugin"
type = LibraryType.GRADLE_PLUGIN
inceptionYear = "2019"
description = "Android Benchmark Gradle Plugin"
diff --git a/biometric/biometric-ktx/samples/build.gradle b/biometric/biometric-ktx/samples/build.gradle
index 0f995ee..5d20e55 100644
--- a/biometric/biometric-ktx/samples/build.gradle
+++ b/biometric/biometric-ktx/samples/build.gradle
@@ -28,7 +28,7 @@
}
androidx {
- name = "AndroidX Biometric Samples"
+ name = "Biometric Samples"
type = LibraryType.SAMPLES
inceptionYear = "2021"
description = "Contains the sample code for the AndroidX Biometric library"
diff --git a/biometric/biometric/src/test/resources/robolectric.properties b/biometric/biometric/src/test/resources/robolectric.properties
index 7946f01..69fde47 100644
--- a/biometric/biometric/src/test/resources/robolectric.properties
+++ b/biometric/biometric/src/test/resources/robolectric.properties
@@ -1 +1,3 @@
-# robolectric properties
\ No newline at end of file
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/bluetooth/bluetooth-testing/build.gradle b/bluetooth/bluetooth-testing/build.gradle
index a9ac8a5..e0f587c 100644
--- a/bluetooth/bluetooth-testing/build.gradle
+++ b/bluetooth/bluetooth-testing/build.gradle
@@ -27,7 +27,7 @@
}
androidx {
- name = "AndroidX Bluetooth Testing"
+ name = "Bluetooth Testing"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2022"
description = "Test utilities for AndroidX Bluetooth"
diff --git a/bluetooth/bluetooth/build.gradle b/bluetooth/bluetooth/build.gradle
index c455f0b..8e7f695 100644
--- a/bluetooth/bluetooth/build.gradle
+++ b/bluetooth/bluetooth/build.gradle
@@ -40,7 +40,7 @@
}
androidx {
- name = "AndroidX Bluetooth"
+ name = "Bluetooth"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2022"
description = "AndroidX Bluetooth Library"
diff --git a/browser/browser/build.gradle b/browser/browser/build.gradle
index de7fa00..195ec5c 100644
--- a/browser/browser/build.gradle
+++ b/browser/browser/build.gradle
@@ -51,8 +51,8 @@
}
androidx {
- name = "Android Support Custom Tabs"
+ name = "Browser"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2015"
- description = "Android Support Custom Tabs"
+ description = "Provides support for embedding Custom Tabs in an app."
}
diff --git a/browser/browser/src/test/resources/robolectric.properties b/browser/browser/src/test/resources/robolectric.properties
index 7946f01..69fde47 100644
--- a/browser/browser/src/test/resources/robolectric.properties
+++ b/browser/browser/src/test/resources/robolectric.properties
@@ -1 +1,3 @@
-# robolectric properties
\ No newline at end of file
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/buildSrc-tests/max-dep-versions/buildSrc-tests-max-dep-versions-dep/build.gradle b/buildSrc-tests/max-dep-versions/buildSrc-tests-max-dep-versions-dep/build.gradle
index 06e9e6e..eae2213 100644
--- a/buildSrc-tests/max-dep-versions/buildSrc-tests-max-dep-versions-dep/build.gradle
+++ b/buildSrc-tests/max-dep-versions/buildSrc-tests-max-dep-versions-dep/build.gradle
@@ -11,7 +11,7 @@
}
androidx {
- name = "Sample Library"
+ name = "Sample"
publish = Publish.SNAPSHOT_AND_RELEASE
type = LibraryType.SAMPLES
inceptionYear = "2020"
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index d5f73f51..bb7940d 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -586,6 +586,7 @@
extension.type == LibraryType.UNSET
if (mavenGroup != null && isProbablyPublished && extension.shouldPublish()) {
validateProjectStructure(mavenGroup.group)
+ validateProjectMavenName(extension.name.get(), mavenGroup.group)
}
}
}
@@ -1135,46 +1136,6 @@
return false
}
-private const val GROUP_PREFIX = "androidx."
-
-/**
- * Validates the project structure against Jetpack guidelines.
- */
-fun Project.validateProjectStructure(groupId: String) {
- if (!project.isValidateProjectStructureEnabled()) {
- return
- }
-
- val shortGroupId = if (groupId.startsWith(GROUP_PREFIX)) {
- groupId.substring(GROUP_PREFIX.length)
- } else {
- groupId
- }
-
- // Fully-qualified Gradle project name should match the Maven coordinate.
- val expectName = ":${shortGroupId.replace(".",":")}:${project.name}"
- val actualName = project.path
- if (expectName != actualName) {
- throw GradleException(
- "Invalid project structure! Expected $expectName as project name, found $actualName"
- )
- }
-
- // Project directory should match the Maven coordinate.
- val expectDir = shortGroupId.replace(".", File.separator) +
- "${File.separator}${project.name}"
- // Canonical projectDir is needed because sometimes, at least in tests, on OSX, supportRoot
- // starts with /var, and projectDir starts with /private/var (which are the same thing)
- val canonicalProjectDir = project.projectDir.canonicalFile
- val actualDir =
- canonicalProjectDir.toRelativeString(project.getSupportRootFolder().canonicalFile)
- if (expectDir != actualDir) {
- throw GradleException(
- "Invalid project structure! Expected $expectDir as project directory, found $actualDir"
- )
- }
-}
-
fun Project.validateMultiplatformPluginHasNotBeenApplied() {
if (plugins.hasPlugin(KotlinMultiplatformPluginWrapper::class.java)) {
throw GradleException(
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ProjectConfigValidators.kt b/buildSrc/private/src/main/kotlin/androidx/build/ProjectConfigValidators.kt
new file mode 100644
index 0000000..bbe7f2a
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ProjectConfigValidators.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.build
+
+import java.io.File
+import org.gradle.api.GradleException
+import org.gradle.api.Project
+
+// Translate common phrases and marketing names into Maven name component equivalents.
+private val mavenNameMap = mapOf(
+ "android for cars" to "car",
+ "android wear" to "wear",
+ "internationalization" to "i18n",
+ "kotlin extensions" to "ktx",
+ "lint checks" to "lint",
+ "material components" to "material",
+ "material3 components" to "material3",
+ "workmanager" to "work",
+ "windowmanager" to "window",
+)
+
+// Allow a small set of common Maven name components that don't need to appear in the project name.
+private val mavenNameAllowlist = setOf(
+ "extension",
+ "extensions",
+ "for",
+ "integration",
+ "with",
+)
+
+/**
+ * Validates the project's Maven name against Jetpack guidelines.
+ */
+fun Project.validateProjectMavenName(mavenName: String?, groupId: String) {
+ if (mavenName == null) return
+
+ // Tokenize the Maven name into components. This is *very* permissive regarding separators, and
+ // we may want to revisit that policy in the future.
+ val nameComponents = mavenName.lowercase().let { name ->
+ mavenNameMap.entries.fold(name) { newName, entry ->
+ newName.replace(entry.key, entry.value)
+ }
+ }.split(" ", ",", ":", "-").toMutableList() - mavenNameAllowlist
+
+ // Remaining components *must* appear in the Maven coordinate. Shortening long (>10 char) words
+ // to five letters or more is allowed, as is changing the pluralization of words.
+ nameComponents.find { nameComponent ->
+ !name.contains(nameComponent) && !groupId.contains(nameComponent) &&
+ !(nameComponent.length > 10 && name.contains(nameComponent.substring(0, 5))) &&
+ !(nameComponent.endsWith("s") && name.contains(nameComponent.dropLast(1)))
+ }?.let { missingComponent ->
+ throw GradleException(
+ "Invalid Maven name! Found \"$missingComponent\" in Maven name for $displayName, but " +
+ "not project name.\n\nConsider removing \"$missingComponent\" from \"$mavenName\"."
+ )
+ }
+}
+
+private const val GROUP_PREFIX = "androidx."
+
+/**
+ * Validates the project structure against Jetpack guidelines.
+ */
+fun Project.validateProjectStructure(groupId: String) {
+ if (!project.isValidateProjectStructureEnabled()) {
+ return
+ }
+
+ val shortGroupId = if (groupId.startsWith(GROUP_PREFIX)) {
+ groupId.substring(GROUP_PREFIX.length)
+ } else {
+ groupId
+ }
+
+ // Fully-qualified Gradle project name should match the Maven coordinate.
+ val expectName = ":${shortGroupId.replace(".",":")}:${project.name}"
+ val actualName = project.path
+ if (expectName != actualName) {
+ throw GradleException(
+ "Invalid project structure! Expected $expectName as project name, found $actualName"
+ )
+ }
+
+ // Project directory should match the Maven coordinate.
+ val expectDir = shortGroupId.replace(".", File.separator) +
+ "${File.separator}${project.name}"
+ // Canonical projectDir is needed because sometimes, at least in tests, on OSX, supportRoot
+ // starts with /var, and projectDir starts with /private/var (which are the same thing)
+ val canonicalProjectDir = project.projectDir.canonicalFile
+ val actualDir =
+ canonicalProjectDir.toRelativeString(project.getSupportRootFolder().canonicalFile)
+ if (expectDir != actualDir) {
+ throw GradleException(
+ "Invalid project structure! Expected $expectDir as project directory, found $actualDir"
+ )
+ }
+}
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt b/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
index e0087b3..2e6bddb 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
@@ -34,7 +34,7 @@
* Either an integer value or a pre-release platform code, prefixed with "android-" (ex.
* "android-28" or "android-Q") as you would see within the SDK's platforms directory.
*/
- const val COMPILE_SDK_VERSION = "android-33-ext5"
+ const val COMPILE_SDK_VERSION = "android-34"
/**
* The Android SDK version to use for targetSdkVersion meta-data.
@@ -47,7 +47,7 @@
* order for tests to run on devices running released versions of the Android OS. If this is
* set to a pre-release version, tests will only be able to run on pre-release devices.
*/
- const val TARGET_SDK_VERSION = 33
+ const val TARGET_SDK_VERSION = 34
/**
* Returns the build tools version that should be used for the project.
diff --git a/busytown/impl/check_translations.sh b/busytown/impl/check_translations.sh
index 35501ad..0f9e440 100755
--- a/busytown/impl/check_translations.sh
+++ b/busytown/impl/check_translations.sh
@@ -20,6 +20,7 @@
find . \
\( \
-iname '*sample*' \
+ -o -iname '*demo*' \
-o -iname '*donottranslate*' \
-o -iname '*debug*' \
-o -iname '*test*' \
diff --git a/camera/camera-camera2-pipe-integration/build.gradle b/camera/camera-camera2-pipe-integration/build.gradle
index 5ba8cb5..3a1f95e 100644
--- a/camera/camera-camera2-pipe-integration/build.gradle
+++ b/camera/camera-camera2-pipe-integration/build.gradle
@@ -115,11 +115,11 @@
}
androidx {
- name = "Jetpack Camera Camera Pipe Integration Library"
+ name = "Camera2 Pipe Integration"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.CAMERA_PIPE
inceptionYear = "2020"
- description = "A CameraPipe implementation of CameraX, a library providing a consistent and " +
- "reliable camera foundation that enables great camera driven experiences across all " +
- "of Android."
+ description = "A Camera2 Pipe implementation of CameraX, a library providing a consistent " +
+ "and reliable camera foundation that enables great camera driven experiences across " +
+ "all of Android."
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/resources/robolectric.properties b/camera/camera-camera2-pipe-integration/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..69fde47
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/camera/camera-camera2-pipe-testing/build.gradle b/camera/camera-camera2-pipe-testing/build.gradle
index fcf6f79..1d31206 100644
--- a/camera/camera-camera2-pipe-testing/build.gradle
+++ b/camera/camera-camera2-pipe-testing/build.gradle
@@ -60,12 +60,12 @@
}
androidx {
- name = "Jetpack Camera Camera Pipe Testing Library"
+ name = "Camera2 Pipe Testing"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.CAMERA_PIPE
runApiTasks = new RunApiTasks.No("CameraPipe is an implementation detail of other libraries.")
inceptionYear = "2020"
- description = "Testing components for the Jetpack CameraPipe Library, a library providing a " +
+ description = "Testing components for the Camera2 Pipe Library, a library providing a " +
"consistent and reliable camera foundation that enables great camera driven " +
"experiences across all of Android."
}
diff --git a/camera/camera-camera2-pipe/build.gradle b/camera/camera-camera2-pipe/build.gradle
index f339499..e2a6054 100644
--- a/camera/camera-camera2-pipe/build.gradle
+++ b/camera/camera-camera2-pipe/build.gradle
@@ -71,7 +71,7 @@
}
androidx {
- name = "Jetpack Camera Pipe"
+ name = "Camera2 Pipe"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.CAMERA_PIPE
runApiTasks = new RunApiTasks.No("CameraPipe is an implementation detail of other libraries.")
diff --git a/camera/camera-camera2/build.gradle b/camera/camera-camera2/build.gradle
index 8c0100e..b4ad3b0 100644
--- a/camera/camera-camera2/build.gradle
+++ b/camera/camera-camera2/build.gradle
@@ -88,7 +88,7 @@
}
androidx {
- name = "Jetpack Camera Library Camera2 Implementation/Extensions"
+ name = "Camera2"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
description = "Camera2 implementation and extensions for the Jetpack Camera Library, a " +
diff --git a/camera/camera-camera2/src/test/resources/robolectric.properties b/camera/camera-camera2/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..69fde47
--- /dev/null
+++ b/camera/camera-camera2/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/camera/camera-core/build.gradle b/camera/camera-core/build.gradle
index c789d6d..90360e1 100644
--- a/camera/camera-core/build.gradle
+++ b/camera/camera-core/build.gradle
@@ -111,7 +111,7 @@
}
androidx {
- name = "Jetpack Camera Core Library"
+ name = "Camera Core"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
description = "Core components for the Jetpack Camera Library, a library providing a " +
diff --git a/camera/camera-core/src/test/resources/robolectric.properties b/camera/camera-core/src/test/resources/robolectric.properties
index 7946f01..69fde47 100644
--- a/camera/camera-core/src/test/resources/robolectric.properties
+++ b/camera/camera-core/src/test/resources/robolectric.properties
@@ -1 +1,3 @@
-# robolectric properties
\ No newline at end of file
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/camera/camera-effects-still-portrait/build.gradle b/camera/camera-effects-still-portrait/build.gradle
index e6d14aa..647567c 100644
--- a/camera/camera-effects-still-portrait/build.gradle
+++ b/camera/camera-effects-still-portrait/build.gradle
@@ -32,7 +32,7 @@
namespace "androidx.camera.effects.stillportrait"
}
androidx {
- name = "Jetpack Camera Portrait Still Effect Library"
+ name = "Camera Effects: Still Portrait"
publish = Publish.SNAPSHOT_ONLY
inceptionYear = "2022"
runApiTasks = new RunApiTasks.Yes()
diff --git a/camera/camera-effects/build.gradle b/camera/camera-effects/build.gradle
index d6fc08d..858866e 100644
--- a/camera/camera-effects/build.gradle
+++ b/camera/camera-effects/build.gradle
@@ -32,7 +32,7 @@
namespace "androidx.camera.effects"
}
androidx {
- name = "Jetpack Camera Effects Library"
+ name = "Camera Effects"
publish = Publish.NONE
inceptionYear = "2022"
runApiTasks = new RunApiTasks.Yes()
diff --git a/camera/camera-extensions-stub/build.gradle b/camera/camera-extensions-stub/build.gradle
index de2b943..efca9c0 100644
--- a/camera/camera-extensions-stub/build.gradle
+++ b/camera/camera-extensions-stub/build.gradle
@@ -25,7 +25,7 @@
}
androidx {
- name = "Jetpack Camera Library OEM Extensions Stub"
+ name = "Camera OEM Extensions Stub"
publish = Publish.NONE
inceptionYear = "2019"
diff --git a/camera/camera-extensions/build.gradle b/camera/camera-extensions/build.gradle
index 1627bec..4cb5a01 100644
--- a/camera/camera-extensions/build.gradle
+++ b/camera/camera-extensions/build.gradle
@@ -81,7 +81,7 @@
}
androidx {
- name = "Jetpack Camera Library OEM Extensions"
+ name = "Camera Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
diff --git a/camera/camera-lifecycle/build.gradle b/camera/camera-lifecycle/build.gradle
index 7d0119c..4f7b607 100644
--- a/camera/camera-lifecycle/build.gradle
+++ b/camera/camera-lifecycle/build.gradle
@@ -61,7 +61,7 @@
}
androidx {
- name = "Jetpack Camera Lifecycle Library"
+ name = "Camera Lifecycle"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
description = "Lifecycle components for the Jetpack Camera Library, a library providing a " +
diff --git a/camera/camera-mlkit-vision/build.gradle b/camera/camera-mlkit-vision/build.gradle
index f0f3bad..e569d1f 100644
--- a/camera/camera-mlkit-vision/build.gradle
+++ b/camera/camera-mlkit-vision/build.gradle
@@ -58,7 +58,7 @@
}
androidx {
- name = "Jetpack Camera MLKit Vision Library"
+ name = "Camera MLKit Vision"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2022"
runApiTasks = new RunApiTasks.Yes()
diff --git a/camera/camera-testing/build.gradle b/camera/camera-testing/build.gradle
index 177e3db..3c9f0b7 100644
--- a/camera/camera-testing/build.gradle
+++ b/camera/camera-testing/build.gradle
@@ -91,7 +91,7 @@
}
androidx {
- name = "Jetpack Camera Testing Library"
+ name = "Camera Testing"
publish = Publish.SNAPSHOT_AND_RELEASE
runApiTasks = new RunApiTasks.No("Internal testing library without any release plan yet.")
inceptionYear = "2019"
diff --git a/camera/camera-testlib-extensions/build.gradle b/camera/camera-testlib-extensions/build.gradle
index 886b875..849e115 100644
--- a/camera/camera-testlib-extensions/build.gradle
+++ b/camera/camera-testlib-extensions/build.gradle
@@ -39,7 +39,7 @@
}
androidx {
- name = "Jetpack Camera Extensions Example Library"
+ name = "Camera Extensions Example"
publish = Publish.NONE
inceptionYear = "2019"
description = "Example extension implementation for the Jetpack Camera Library, a library providing a " +
diff --git a/camera/camera-video/build.gradle b/camera/camera-video/build.gradle
index ff70608..d62ee62 100644
--- a/camera/camera-video/build.gradle
+++ b/camera/camera-video/build.gradle
@@ -87,7 +87,7 @@
}
androidx {
- name = "Jetpack Camera Video Library"
+ name = "Camera Video"
publish = Publish.SNAPSHOT_AND_RELEASE
runApiTasks = new RunApiTasks.Yes("Need to track API surface before moving to publish")
inceptionYear = "2020"
diff --git a/camera/camera-video/lint-baseline.xml b/camera/camera-video/lint-baseline.xml
index e8b6b9c..303aa35 100644
--- a/camera/camera-video/lint-baseline.xml
+++ b/camera/camera-video/lint-baseline.xml
@@ -37,4 +37,13 @@
file="src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt"/>
</issue>
+ <issue
+ id="IgnoreClassLevelDetector"
+ message="@Ignore should not be used at the class level. Move the annotation to each test individually."
+ errorLine1="@Ignore("b/274840083")"
+ errorLine2="~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/test/java/androidx/camera/video/internal/audio/AudioSourceTest.kt"/>
+ </issue>
+
</issues>
diff --git a/camera/camera-view/build.gradle b/camera/camera-view/build.gradle
index fe34a08..2476a37 100644
--- a/camera/camera-view/build.gradle
+++ b/camera/camera-view/build.gradle
@@ -80,7 +80,7 @@
namespace "androidx.camera.view"
}
androidx {
- name = "Jetpack Camera View Library"
+ name = "Camera View"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
diff --git a/camera/camera-view/src/test/resources/robolectric.properties b/camera/camera-view/src/test/resources/robolectric.properties
index 7946f01..69fde47 100644
--- a/camera/camera-view/src/test/resources/robolectric.properties
+++ b/camera/camera-view/src/test/resources/robolectric.properties
@@ -1 +1,3 @@
-# robolectric properties
\ No newline at end of file
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/camera/camera-viewfinder-core/build.gradle b/camera/camera-viewfinder-core/build.gradle
index f2a2a9e..840c124 100644
--- a/camera/camera-viewfinder-core/build.gradle
+++ b/camera/camera-viewfinder-core/build.gradle
@@ -33,9 +33,9 @@
}
androidx {
- name = "androidx.camera:camera-viewfinder-core"
+ name = "Camera ViewFinder Core"
type = LibraryType.PUBLISHED_LIBRARY
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2023"
- description = "core dependencies for viewfinder"
+ description = "Core dependencies for ViewFinder"
}
diff --git a/camera/camera-viewfinder/build.gradle b/camera/camera-viewfinder/build.gradle
index 39c752c..e77d559 100644
--- a/camera/camera-viewfinder/build.gradle
+++ b/camera/camera-viewfinder/build.gradle
@@ -80,7 +80,7 @@
}
androidx {
- name = "androidx.camera:camera-viewfinder"
+ name = "Camera ViewFinder"
publish = Publish.SNAPSHOT_AND_RELEASE
runApiTasks = new RunApiTasks.Yes("Need to track API surface before moving to publish")
inceptionYear = "2022"
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
index 1d771cc..3a0403c 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
@@ -609,21 +609,29 @@
// For testing
// -----------------
+ /**
+ */
@VisibleForTesting
LifecycleCameraController getCameraController() {
return mCameraController;
}
+ /**
+ */
@VisibleForTesting
void setWrappedAnalyzer(@Nullable ImageAnalysis.Analyzer analyzer) {
mWrappedAnalyzer = analyzer;
}
+ /**
+ */
@VisibleForTesting
PreviewView getPreviewView() {
return mPreviewView;
}
+ /**
+ */
@VisibleForTesting
int getSensorRotation() {
return mRotation;
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingSurfaceEffect.kt b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingSurfaceEffect.kt
index 50291a7..ed428d7 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingSurfaceEffect.kt
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingSurfaceEffect.kt
@@ -29,4 +29,4 @@
fun release() {
(surfaceProcessor as? ToneMappingSurfaceProcessor)?.release()
}
-}
\ No newline at end of file
+}
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml b/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml
index abdbd23..89a1fe8 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml
@@ -173,4 +173,4 @@
android:id="@+id/torch_state_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/camera/integration-tests/viewtestapp/src/main/res/menu/actionbar_menu.xml b/camera/integration-tests/viewtestapp/src/main/res/menu/actionbar_menu.xml
index 4a8d889..3192b6b 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/menu/actionbar_menu.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/menu/actionbar_menu.xml
@@ -36,4 +36,4 @@
android:id="@+id/mlkit"
android:title="@string/mlkit"
app:showAsAction="never" />
-</menu>
\ No newline at end of file
+</menu>
diff --git a/car/app/app-automotive/build.gradle b/car/app/app-automotive/build.gradle
index a496e2d..7361f0e 100644
--- a/car/app/app-automotive/build.gradle
+++ b/car/app/app-automotive/build.gradle
@@ -72,7 +72,7 @@
}
androidx {
- name = "Android for Cars App Library Automotive Extension"
+ name = "Android for Cars App Automotive Extension"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2021"
description = "Automotive OS specific functionality to build navigation, parking, and charging apps for cars"
diff --git a/car/app/app-automotive/src/test/resources/robolectric.properties b/car/app/app-automotive/src/test/resources/robolectric.properties
index 80e2a6f..69fde47 100644
--- a/car/app/app-automotive/src/test/resources/robolectric.properties
+++ b/car/app/app-automotive/src/test/resources/robolectric.properties
@@ -1 +1,3 @@
# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/car/app/app-projected/build.gradle b/car/app/app-projected/build.gradle
index 5754a49..454cbec 100644
--- a/car/app/app-projected/build.gradle
+++ b/car/app/app-projected/build.gradle
@@ -60,7 +60,7 @@
}
androidx {
- name = "Android for Cars App Library Projected Extension"
+ name = "Android for Cars App Projected Extension"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2021"
description = "Android Auto Projected specific funationaltiy to build navigation, parking, and charging apps for cars"
diff --git a/car/app/app-projected/src/test/resources/robolectric.properties b/car/app/app-projected/src/test/resources/robolectric.properties
index 7946f01..69fde47 100644
--- a/car/app/app-projected/src/test/resources/robolectric.properties
+++ b/car/app/app-projected/src/test/resources/robolectric.properties
@@ -1 +1,3 @@
-# robolectric properties
\ No newline at end of file
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/car/app/app-testing/src/test/resources/robolectric.properties b/car/app/app-testing/src/test/resources/robolectric.properties
index 71111c5..69fde47 100644
--- a/car/app/app-testing/src/test/resources/robolectric.properties
+++ b/car/app/app-testing/src/test/resources/robolectric.properties
@@ -1,17 +1,3 @@
-#
-# Copyright 2021 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/car/app/app/build.gradle b/car/app/app/build.gradle
index 43d2789..e31bbf1 100644
--- a/car/app/app/build.gradle
+++ b/car/app/app/build.gradle
@@ -100,7 +100,7 @@
}
androidx {
- name = "Android for Cars App Library"
+ name = "Android for Cars App"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2020"
description = "Build navigation, parking, and charging apps for Android Auto"
diff --git a/car/app/app/src/test/resources/robolectric.properties b/car/app/app/src/test/resources/robolectric.properties
index 80e2a6f..69fde47 100644
--- a/car/app/app/src/test/resources/robolectric.properties
+++ b/car/app/app/src/test/resources/robolectric.properties
@@ -1 +1,3 @@
# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/cardview/cardview/build.gradle b/cardview/cardview/build.gradle
index 239abf1..13cc235 100644
--- a/cardview/cardview/build.gradle
+++ b/cardview/cardview/build.gradle
@@ -11,7 +11,7 @@
}
androidx {
- name = "Android Support CardView"
+ name = "CardView"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2011"
description = "Android Support CardView"
diff --git a/collection/collection-benchmark/build.gradle b/collection/collection-benchmark/build.gradle
index dc0cf11..931098c 100644
--- a/collection/collection-benchmark/build.gradle
+++ b/collection/collection-benchmark/build.gradle
@@ -105,7 +105,7 @@
}
androidx {
- name = "AndroidX Collections Benchmarks (Android / iOS)"
+ name = "Collections Benchmarks (Android / iOS)"
inceptionYear = "2022"
description = "AndroidX Collections Benchmarks (Android / iOS)"
}
diff --git a/collection/collection/build.gradle b/collection/collection/build.gradle
index 0ac4f14..47b1d5e 100644
--- a/collection/collection/build.gradle
+++ b/collection/collection/build.gradle
@@ -136,7 +136,7 @@
}
androidx {
- name = "Android Support Library collections"
+ name = "collections"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2018"
description = "Standalone efficient collections."
diff --git a/collection/integration-tests/testapp/build.gradle b/collection/integration-tests/testapp/build.gradle
index a8d529f..67a0ce6 100644
--- a/collection/integration-tests/testapp/build.gradle
+++ b/collection/integration-tests/testapp/build.gradle
@@ -28,7 +28,7 @@
}
androidx {
- name = "AndroidX Collection Integration Tests"
+ name = "Collection Integration Tests"
publish = Publish.NONE
inceptionYear = "2021"
description = "AndroidX Collection Integration Tests"
diff --git a/compose/animation/animation-core/samples/build.gradle b/compose/animation/animation-core/samples/build.gradle
index 160f365..735da8e 100644
--- a/compose/animation/animation-core/samples/build.gradle
+++ b/compose/animation/animation-core/samples/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "AndroidX Compose UI Animation Library Core Classes Samples"
+ name = "Compose UI Animation Core Classes Samples"
type = LibraryType.SAMPLES
inceptionYear = "2019"
description = "Contains the sample code for the Androidx Compose UI Animation Core Classes"
diff --git a/compose/animation/animation-graphics/samples/build.gradle b/compose/animation/animation-graphics/samples/build.gradle
index 2b0e793..c317288 100644
--- a/compose/animation/animation-graphics/samples/build.gradle
+++ b/compose/animation/animation-graphics/samples/build.gradle
@@ -38,7 +38,7 @@
}
androidx {
- name = "AndroidX Compose UI Animation Graphics Library Samples"
+ name = "Compose UI Animation Graphics Samples"
type = LibraryType.SAMPLES
inceptionYear = "2021"
description = "Contains the sample code for the Androidx Compose UI Animation Graphics Library"
diff --git a/compose/animation/animation/samples/build.gradle b/compose/animation/animation/samples/build.gradle
index d355cdf..9fd59da 100644
--- a/compose/animation/animation/samples/build.gradle
+++ b/compose/animation/animation/samples/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "AndroidX Compose UI Animation Library Samples"
+ name = "Compose UI Animation Samples"
type = LibraryType.SAMPLES
inceptionYear = "2019"
description = "Contains the sample code for the Androidx Compose UI Animation Library"
diff --git a/compose/compiler/compiler-daemon/integration-tests/build.gradle b/compose/compiler/compiler-daemon/integration-tests/build.gradle
index afda48a..5416516 100644
--- a/compose/compiler/compiler-daemon/integration-tests/build.gradle
+++ b/compose/compiler/compiler-daemon/integration-tests/build.gradle
@@ -30,7 +30,7 @@
}
androidx {
- name = "AndroidX Compiler Daemon CLI Tests"
+ name = "Compiler Daemon CLI Tests"
type = LibraryType.COMPILER_DAEMON_TEST
inceptionYear = "2021"
description = "Contains test for the compose compiler daemon"
diff --git a/compose/compiler/compiler-hosted/build.gradle b/compose/compiler/compiler-hosted/build.gradle
index fb76744..e4ce28e 100644
--- a/compose/compiler/compiler-hosted/build.gradle
+++ b/compose/compiler/compiler-hosted/build.gradle
@@ -41,7 +41,7 @@
}
androidx {
- name = "AndroidX Compose Hosted Compiler Plugin"
+ name = "Compose Hosted Compiler Plugin"
// This is only published because that is required when exporting it to g3.
// Nobody should ever get this artifact from maven; just from studio or from source
type = LibraryType.COMPILER_PLUGIN
diff --git a/compose/compiler/compiler-hosted/integration-tests/build.gradle b/compose/compiler/compiler-hosted/integration-tests/build.gradle
index b874154..00a111c 100644
--- a/compose/compiler/compiler-hosted/integration-tests/build.gradle
+++ b/compose/compiler/compiler-hosted/integration-tests/build.gradle
@@ -82,7 +82,7 @@
}
androidx {
- name = "AndroidX Compiler CLI Tests"
+ name = "Compiler CLI Tests"
publish = Publish.NONE
inceptionYear = "2019"
description = "Contains test for the compose compiler plugin"
diff --git a/compose/desktop/desktop/build.gradle b/compose/desktop/desktop/build.gradle
index 46e9c83..6d9243e 100644
--- a/compose/desktop/desktop/build.gradle
+++ b/compose/desktop/desktop/build.gradle
@@ -98,7 +98,7 @@
}
androidx {
- name = "Jetpack Compose desktop implementation"
+ name = "Compose desktop implementation"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2020"
legacyDisableKotlinStrictApiMode = true
diff --git a/compose/foundation/foundation-layout/samples/build.gradle b/compose/foundation/foundation-layout/samples/build.gradle
index dd396a9..3e07527 100644
--- a/compose/foundation/foundation-layout/samples/build.gradle
+++ b/compose/foundation/foundation-layout/samples/build.gradle
@@ -39,7 +39,7 @@
}
androidx {
- name = "AndroidX Compose UI Core Layout Classes Samples"
+ name = "Compose UI Core Layout Classes Samples"
type = LibraryType.SAMPLES
inceptionYear = "2019"
description = "Contains the sample code for the Androidx Compose UI Core Layout Classes"
diff --git a/compose/foundation/foundation/samples/build.gradle b/compose/foundation/foundation/samples/build.gradle
index 38d57ca..d6871de 100644
--- a/compose/foundation/foundation/samples/build.gradle
+++ b/compose/foundation/foundation/samples/build.gradle
@@ -41,7 +41,7 @@
}
androidx {
- name = "AndroidX Compose UI Foundational Component Samples"
+ name = "Compose UI Foundational Component Samples"
type = LibraryType.SAMPLES
inceptionYear = "2019"
description = "Contains the sample code for the Androidx Compose UI Foundational Components"
diff --git a/compose/material/material-icons-core/samples/build.gradle b/compose/material/material-icons-core/samples/build.gradle
index 040d910..0977215 100644
--- a/compose/material/material-icons-core/samples/build.gradle
+++ b/compose/material/material-icons-core/samples/build.gradle
@@ -36,7 +36,7 @@
}
androidx {
- name = "AndroidX Compose UI Core Material Icons Samples"
+ name = "Compose UI Core Material Icons Samples"
type = LibraryType.SAMPLES
inceptionYear = "2019"
description = "Contains the sample code for the Androidx Compose UI Core Material Icons"
diff --git a/compose/material/material/samples/build.gradle b/compose/material/material/samples/build.gradle
index 0b5f612..73cef10 100644
--- a/compose/material/material/samples/build.gradle
+++ b/compose/material/material/samples/build.gradle
@@ -40,7 +40,7 @@
}
androidx {
- name = "AndroidX Compose Material Components Samples"
+ name = "Compose Material Components Samples"
type = LibraryType.SAMPLES
inceptionYear = "2019"
description = "Contains the sample code for the AndroidX Compose Material components."
diff --git a/compose/material3/material3-window-size-class/samples/build.gradle b/compose/material3/material3-window-size-class/samples/build.gradle
index 4dc7891..71ffc63d1 100644
--- a/compose/material3/material3-window-size-class/samples/build.gradle
+++ b/compose/material3/material3-window-size-class/samples/build.gradle
@@ -36,7 +36,7 @@
}
androidx {
- name = "AndroidX Compose Material 3 Window Size Class Samples"
+ name = "Compose Material 3 Window Size Class Samples"
type = LibraryType.SAMPLES
inceptionYear = "2022"
description = "Contains the sample code for the Material 3 Window Size Class APIs"
diff --git a/compose/material3/material3/integration-tests/material3-demos/build.gradle b/compose/material3/material3/integration-tests/material3-demos/build.gradle
index fc83ff8..76ba389 100644
--- a/compose/material3/material3/integration-tests/material3-demos/build.gradle
+++ b/compose/material3/material3/integration-tests/material3-demos/build.gradle
@@ -39,7 +39,7 @@
}
androidx {
- name = "AndroidX Compose Material3 Components Demos"
+ name = "Compose Material3 Components Demos"
publish = Publish.NONE
inceptionYear = "2022"
description = "Contains the demo code for the AndroidX Compose Material 3 components."
diff --git a/compose/material3/material3/samples/build.gradle b/compose/material3/material3/samples/build.gradle
index 075b5f4..eb9d9a3 100644
--- a/compose/material3/material3/samples/build.gradle
+++ b/compose/material3/material3/samples/build.gradle
@@ -47,7 +47,7 @@
}
androidx {
- name = "AndroidX Compose Material3 Components Samples"
+ name = "Compose Material3 Components Samples"
type = LibraryType.SAMPLES
inceptionYear = "2021"
description = "Contains the sample code for the AndroidX Compose Material You components."
diff --git a/compose/runtime/runtime-livedata/samples/build.gradle b/compose/runtime/runtime-livedata/samples/build.gradle
index 78130f2..436d6e9 100644
--- a/compose/runtime/runtime-livedata/samples/build.gradle
+++ b/compose/runtime/runtime-livedata/samples/build.gradle
@@ -34,7 +34,7 @@
}
androidx {
- name = "AndroidX Compose UI Livedata Interop Samples"
+ name = "Compose UI Livedata Interop Samples"
type = LibraryType.SAMPLES
inceptionYear = "2019"
description = "Contains the sample code for the Androidx Compose UI Livedata Interop System"
diff --git a/compose/runtime/runtime-rxjava2/samples/build.gradle b/compose/runtime/runtime-rxjava2/samples/build.gradle
index d73b254..5c9082f4 100644
--- a/compose/runtime/runtime-rxjava2/samples/build.gradle
+++ b/compose/runtime/runtime-rxjava2/samples/build.gradle
@@ -34,7 +34,7 @@
}
androidx {
- name = "AndroidX Compose RxJava 2 Integration Samples"
+ name = "Compose RxJava 2 Integration Samples"
type = LibraryType.SAMPLES
inceptionYear = "2019"
description = "Contains the sample code for the Androidx Compose RxJava 2 Integration System"
diff --git a/compose/runtime/runtime-rxjava3/samples/build.gradle b/compose/runtime/runtime-rxjava3/samples/build.gradle
index aa111a0..9da055d 100644
--- a/compose/runtime/runtime-rxjava3/samples/build.gradle
+++ b/compose/runtime/runtime-rxjava3/samples/build.gradle
@@ -34,7 +34,7 @@
}
androidx {
- name = "AndroidX Compose RxJava 3 Integration Samples"
+ name = "Compose RxJava 3 Integration Samples"
type = LibraryType.SAMPLES
inceptionYear = "2020"
description = "Contains the sample code for the Androidx Compose RxJava 3 Integration System"
diff --git a/compose/runtime/runtime-saveable/samples/build.gradle b/compose/runtime/runtime-saveable/samples/build.gradle
index 8483522..c69d961 100644
--- a/compose/runtime/runtime-saveable/samples/build.gradle
+++ b/compose/runtime/runtime-saveable/samples/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "AndroidX Compose Saved Instance State System Samples"
+ name = "Compose Saved Instance State System Samples"
type = LibraryType.SAMPLES
inceptionYear = "2019"
description = "Contains the sample code for the Androidx Compose Saved Instance State System"
diff --git a/compose/runtime/runtime/samples/build.gradle b/compose/runtime/runtime/samples/build.gradle
index 376b851..a8c279f 100644
--- a/compose/runtime/runtime/samples/build.gradle
+++ b/compose/runtime/runtime/samples/build.gradle
@@ -36,7 +36,7 @@
}
androidx {
- name = "AndroidX Compose Runtime Samples"
+ name = "Compose Runtime Samples"
type = LibraryType.SAMPLES
inceptionYear = "2019"
description = "Contains the sample code for the Compose runtime"
diff --git a/compose/ui/ui-graphics/samples/build.gradle b/compose/ui/ui-graphics/samples/build.gradle
index b255806..15643a9 100644
--- a/compose/ui/ui-graphics/samples/build.gradle
+++ b/compose/ui/ui-graphics/samples/build.gradle
@@ -36,7 +36,7 @@
}
androidx {
- name = "AndroidX Compose UI Graphics Components Samples"
+ name = "Compose UI Graphics Components Samples"
type = LibraryType.SAMPLES
inceptionYear = "2019"
description = "Contains the sample code for the Androidx Compose UI Graphics Components"
diff --git a/compose/ui/ui-inspection/build.gradle b/compose/ui/ui-inspection/build.gradle
index d7db14f..9ee96cd9 100644
--- a/compose/ui/ui-inspection/build.gradle
+++ b/compose/ui/ui-inspection/build.gradle
@@ -62,7 +62,7 @@
}
androidx {
- name = "Android Compose Layout Inspector"
+ name = "Compose Layout Inspector"
type = LibraryType.IDE_PLUGIN
inceptionYear = "2021"
description = "Compose layout inspector. Exposes information to our tools for better IDE support."
diff --git a/compose/ui/ui-test/samples/build.gradle b/compose/ui/ui-test/samples/build.gradle
index 668bb91..0845c78 100644
--- a/compose/ui/ui-test/samples/build.gradle
+++ b/compose/ui/ui-test/samples/build.gradle
@@ -38,7 +38,7 @@
}
androidx {
- name = "AndroidX Compose Testing Samples"
+ name = "Compose Testing Samples"
type = LibraryType.SAMPLES
inceptionYear = "2022"
description = "Contains samples for AndroidX Compose Testing."
diff --git a/compose/ui/ui-text-google-fonts/build.gradle b/compose/ui/ui-text-google-fonts/build.gradle
index d8c49be..f557000 100644
--- a/compose/ui/ui-text-google-fonts/build.gradle
+++ b/compose/ui/ui-text-google-fonts/build.gradle
@@ -42,7 +42,7 @@
}
androidx {
- name = "AndroidX Compose Google Fonts integration"
+ name = "Compose Google Fonts integration"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2022"
description = "Compose Downloadable Fonts integration for Google Fonts"
diff --git a/compose/ui/ui-text/benchmark/lint-baseline.xml b/compose/ui/ui-text/benchmark/lint-baseline.xml
index 94985f2..6431b9c 100644
--- a/compose/ui/ui-text/benchmark/lint-baseline.xml
+++ b/compose/ui/ui-text/benchmark/lint-baseline.xml
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.4.0-alpha08" type="baseline" client="gradle" dependencies="false" name="AGP (7.4.0-alpha08)" variant="all" version="7.4.0-alpha08">
+<issues format="6" by="lint 8.1.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta01)" variant="all" version="8.1.0-beta01">
<issue
id="SoonBlockedPrivateApi"
- message="Reflective access to freeTextLayoutCaches will throw an exception when targeting API 33 and above"
+ message="Reflective access to freeTextLayoutCaches will throw an exception when targeting API 34 and above"
errorLine1=" val freeCaches = Canvas::class.java.getDeclaredMethod("freeTextLayoutCaches")"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
diff --git a/compose/ui/ui-text/samples/build.gradle b/compose/ui/ui-text/samples/build.gradle
index 507a2ddb..a6d5aa4 100644
--- a/compose/ui/ui-text/samples/build.gradle
+++ b/compose/ui/ui-text/samples/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "AndroidX Compose UI Text Core Samples"
+ name = "Compose UI Text Core Samples"
type = LibraryType.SAMPLES
inceptionYear = "2019"
description = "Contains sample code for the Androidx Compose UI Text Core APIs and Utilities"
diff --git a/compose/ui/ui-tooling-preview/build.gradle b/compose/ui/ui-tooling-preview/build.gradle
index d6d3a42..a2aa077 100644
--- a/compose/ui/ui-tooling-preview/build.gradle
+++ b/compose/ui/ui-tooling-preview/build.gradle
@@ -109,7 +109,7 @@
}
androidx {
- name = "Compose Tooling API"
+ name = "Compose UI Preview Tooling"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2021"
description = "Compose tooling library API. This library provides the API required to declare" +
diff --git a/compose/ui/ui-unit/samples/build.gradle b/compose/ui/ui-unit/samples/build.gradle
index a1409de..45bec6c 100644
--- a/compose/ui/ui-unit/samples/build.gradle
+++ b/compose/ui/ui-unit/samples/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "AndroidX Compose UI Simple Unit Classes Samples"
+ name = "Compose UI Simple Unit Classes Samples"
type = LibraryType.SAMPLES
inceptionYear = "2019"
description = "Contains the sample code for the Androidx Compose UI Simple Unit Classes"
diff --git a/compose/ui/ui-viewbinding/samples/build.gradle b/compose/ui/ui-viewbinding/samples/build.gradle
index 29fab3b..88219c6 100644
--- a/compose/ui/ui-viewbinding/samples/build.gradle
+++ b/compose/ui/ui-viewbinding/samples/build.gradle
@@ -44,7 +44,7 @@
}
androidx {
- name = "AndroidX Compose UI Simple Unit Classes Samples"
+ name = "Compose UI Simple Unit Classes Samples"
type = LibraryType.SAMPLES
inceptionYear = "2019"
description = "Contains the sample code for the Androidx Compose UI Simple Unit Classes"
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 3beea81..caff544 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -217,7 +217,7 @@
}
androidx {
- name = "Compose UI primitives"
+ name = "Compose UI"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2019"
description = "Compose UI primitives. This library contains the primitives that form the Compose UI Toolkit, such as drawing, measurement and layout."
diff --git a/compose/ui/ui/samples/build.gradle b/compose/ui/ui/samples/build.gradle
index f2675f6..d201f40 100644
--- a/compose/ui/ui/samples/build.gradle
+++ b/compose/ui/ui/samples/build.gradle
@@ -39,7 +39,7 @@
}
androidx {
- name = "AndroidX Compose UI Core Classes Samples"
+ name = "Compose UI Core Classes Samples"
type = LibraryType.SAMPLES
inceptionYear = "2019"
description = "Contains the sample code for the Androidx Compose UI Core Classes"
diff --git a/concurrent/concurrent-futures-ktx/build.gradle b/concurrent/concurrent-futures-ktx/build.gradle
index 1c2ee74..3aee3a0 100644
--- a/concurrent/concurrent-futures-ktx/build.gradle
+++ b/concurrent/concurrent-futures-ktx/build.gradle
@@ -34,7 +34,7 @@
}
androidx {
- name = "AndroidX Futures Kotlin Extensions"
+ name = "Futures Kotlin Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
description = "Kotlin Extensions for Androidx implementation of Guava's ListenableFuture"
diff --git a/concurrent/concurrent-futures/build.gradle b/concurrent/concurrent-futures/build.gradle
index 2f134c0..94a63aa 100644
--- a/concurrent/concurrent-futures/build.gradle
+++ b/concurrent/concurrent-futures/build.gradle
@@ -29,7 +29,7 @@
}
androidx {
- name = "AndroidX Futures"
+ name = "Futures"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Androidx implementation of Guava's ListenableFuture"
diff --git a/constraintlayout/constraintlayout-compose/build.gradle b/constraintlayout/constraintlayout-compose/build.gradle
index 51a3271..207692f 100644
--- a/constraintlayout/constraintlayout-compose/build.gradle
+++ b/constraintlayout/constraintlayout-compose/build.gradle
@@ -99,7 +99,7 @@
}
androidx {
- name = "Android ConstraintLayout Compose Library"
+ name = "ConstraintLayout Compose"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.CONSTRAINTLAYOUT_COMPOSE
inceptionYear = "2022"
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle b/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle
index 4412d11..7078017 100644
--- a/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle
+++ b/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle
@@ -37,3 +37,4 @@
android {
namespace "androidx.constraintlayout.compose.demos"
}
+
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/demos/lint-baseline.xml b/constraintlayout/constraintlayout-compose/integration-tests/demos/lint-baseline.xml
new file mode 100644
index 0000000..9a0fa82
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/integration-tests/demos/lint-baseline.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.1.0-alpha07" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-beta03)" variant="all" version="8.1.0-alpha07">
+
+ <issue
+ id="LintError"
+ message="Unexpected failure during lint analysis of ChainsDemo.kt (this is a bug in lint or one of the libraries it depends on)

Message: org/jetbrains/uast/visitor/UastVisitor$DefaultImpls

The crash seems to involve the detector `androidx.constraintlayout.compose.lint.ConstraintLayoutDslDetector`.
You can try disabling it with something like this:
 android {
 lint {
 disable "IncorrectReferencesDeclaration", "IncorrectMatchParentUsage", "IncorrectChainMarginsUsage"
 }
 }

Stack: `NoClassDefFoundError:ConstraintLayoutDslDetectorKt$findChildIdentifier$1.afterVisitSimpleNameReferenceExpression(ConstraintLayoutDslDetector.kt:733)←KotlinUSimpleReferenceExpression.accept(KotlinUSimpleReferenceExpression.kt:47)←ConstraintLayoutDslDetectorKt.findChildIdentifier(ConstraintLayoutDslDetector.kt:732)←ConstraintLayoutDslDetector$createUastHandler$1.detectChainParamsUsage(ConstraintLayoutDslDetector.kt:338)←ConstraintLayoutDslDetector$createUastHandler$1.visitCallExpression(ConstraintLayoutDslDetector.kt:144)←UElementVisitor$DispatchPsiVisitor.visitCallExpression(UElementVisitor.kt:511)←UElementVisitor$DelegatingPsiVisitor.visitCallExpression(UElementVisitor.kt:1057)←KotlinUFunctionCallExpression.accept(KotlinUFunctionCallExpression.kt:165)←ImplementationUtilsKt.acceptList(implementationUtils.kt:14)←UBlockExpression.accept(UBlockExpression.kt:21)←UIfExpression.accept(UIfExpression.kt:59)←ImplementationUtilsKt.acceptList(implementationUtils.kt:14)←UBlockExpression.accept(UBlockExpression.kt:21)←ULambdaExpression.accept(ULambdaExpression.kt:40)←ImplementationUtilsKt.acceptList(implementationUtils.kt:14)←KotlinUFunctionCallExpression.accept(KotlinUFunctionCallExpression.kt:169)←ImplementationUtilsKt.acceptList(implementationUtils.kt:14)←KotlinUFunctionCallExpression.accept(KotlinUFunctionCallExpression.kt:169)←ImplementationUtilsKt.acceptList(implementationUtils.kt:14)←UBlockExpression.accept(UBlockExpression.kt:21)←ULambdaExpression.accept(ULambdaExpression.kt:40)←ImplementationUtilsKt.acceptList(implementationUtils.kt:14)←KotlinUFunctionCallExpression.accept(KotlinUFunctionCallExpression.kt:169)←ImplementationUtilsKt.acceptList(implementationUtils.kt:14)←UBlockExpression.accept(UBlockExpression.kt:21)←UMethod.accept(UMethod.kt:45)←ImplementationUtilsKt.acceptList(implementationUtils.kt:14)←AbstractKotlinUClass.accept(AbstractKotlinUClass.kt:99)←ImplementationUtilsKt.acceptList(implementationUtils.kt:14)←UFile.accept(UFile.kt:89)←UastLintUtilsKt.acceptSourceFile(UastLintUtils.kt:735)←UElementVisitor$visitFile$3.run(UElementVisitor.kt:267)←LintClient.runReadAction(LintClient.kt:1700)←LintDriver$LintClientWrapper.runReadAction(LintDriver.kt:2867)←UElementVisitor.visitFile(UElementVisitor.kt:264)←LintDriver$visitUastDetectors$1.run(LintDriver.kt:2165)←LintClient.runReadAction(LintClient.kt:1700)←LintDriver$LintClientWrapper.runReadAction(LintDriver.kt:2867)←LintDriver.visitUastDetectors(LintDriver.kt:2165)←LintDriver.visitUast(LintDriver.kt:2127)←LintDriver.runFileDetectors(LintDriver.kt:1379)←LintDriver.checkProject(LintDriver.kt:1144)←LintDriver.checkProjectRoot(LintDriver.kt:615)←LintDriver.access$checkProjectRoot(LintDriver.kt:170)←LintDriver$analyzeOnly$1.invoke(LintDriver.kt:441)←LintDriver$analyzeOnly$1.invoke(LintDriver.kt:438)←LintDriver.doAnalyze(LintDriver.kt:497)←LintDriver.analyzeOnly(LintDriver.kt:438)←LintCliClient$analyzeOnly$1.invoke(LintCliClient.kt:237)←LintCliClient$analyzeOnly$1.invoke(LintCliClient.kt:237)←LintCliClient.run(LintCliClient.kt:279)←LintCliClient.run$default(LintCliClient.kt:262)←LintCliClient.analyzeOnly(LintCliClient.kt:237)←Main.run(Main.java:1687)←Main.run(Main.java:275)←NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-2)←NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)←DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)←Method.invoke(Method.java:568)←AndroidLintWorkAction.invokeLintMainRunMethod(AndroidLintWorkAction.kt:98)←AndroidLintWorkAction.runLint(AndroidLintWorkAction.kt:87)←AndroidLintWorkAction.execute(AndroidLintWorkAction.kt:62)←DefaultWorkerServer.execute(DefaultWorkerServer.java:63)←NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)←NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)←ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)←NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)←AbstractWorker$1.call(AbstractWorker.java:44)←AbstractWorker$1.call(AbstractWorker.java:41)←DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)←DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:199)←DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)←DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)←DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)←DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)←DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)←DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)←AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)←NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)←DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:169)←FutureTask.run(FutureTask.java:264)←DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:187)←DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:120)←DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:162)←Factories$1.create(Factories.java:31)←DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:249)←DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:109)←DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:114)←DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:157)←DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:126)←Executors$RunnableAdapter.call(Executors.java:539)←FutureTask.run(FutureTask.java:264)←ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)←ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:49)←ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)←ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)←Thread.run(Thread.java:833)`

You can run with --stacktrace or set environment variable `LINT_PRINT_STACKTRACE=true` to dump a full stacktrace to stdout.">
+ <location
+ file="src/main/java/androidx/constraintlayout/compose/demos/ChainsDemo.kt"/>
+ </issue>
+
+</issues>
diff --git a/constraintlayout/constraintlayout-core/build.gradle b/constraintlayout/constraintlayout-core/build.gradle
index ce816fb..a466adb 100644
--- a/constraintlayout/constraintlayout-core/build.gradle
+++ b/constraintlayout/constraintlayout-core/build.gradle
@@ -28,7 +28,7 @@
}
androidx {
- name = "Android ConstraintLayout Core Library"
+ name = "ConstraintLayout Core"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.CONSTRAINTLAYOUT_CORE
inceptionYear = "2022"
diff --git a/constraintlayout/constraintlayout/api/api_lint.ignore b/constraintlayout/constraintlayout/api/api_lint.ignore
index 1f05e12..422e6ca 100644
--- a/constraintlayout/constraintlayout/api/api_lint.ignore
+++ b/constraintlayout/constraintlayout/api/api_lint.ignore
@@ -217,24 +217,6 @@
Invalid nullability on parameter `target` in method `onNestedFling`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
InvalidNullabilityOverride: androidx.constraintlayout.motion.widget.MotionLayout#onNestedPreFling(android.view.View, float, float) parameter #0:
Invalid nullability on parameter `target` in method `onNestedPreFling`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.utils.widget.ImageFilterButton#draw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `canvas` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.utils.widget.ImageFilterView#draw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `canvas` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.utils.widget.MockView#onDraw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `canvas` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.utils.widget.MotionButton#draw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `canvas` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.utils.widget.MotionLabel#onDraw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `canvas` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.widget.ConstraintHelper#onDraw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `canvas` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.widget.Guideline#draw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `canvas` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.widget.Placeholder#onDraw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `canvas` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.widget.ReactiveGuide#draw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `canvas` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
KotlinOperator: androidx.constraintlayout.motion.utils.ViewTimeCycle#get(float, long, android.view.View, androidx.constraintlayout.core.motion.utils.KeyCache):
@@ -349,6 +331,8 @@
Missing nullability on method `getSpans` return
MissingNullability: androidx.constraintlayout.helper.widget.Grid#init(android.util.AttributeSet) parameter #0:
Missing nullability on parameter `attrs` in method `init`
+MissingNullability: androidx.constraintlayout.helper.widget.Grid#onDraw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `onDraw`
MissingNullability: androidx.constraintlayout.helper.widget.Grid#setColumnWeights(String) parameter #0:
Missing nullability on parameter `columnWeights` in method `setColumnWeights`
MissingNullability: androidx.constraintlayout.helper.widget.Grid#setRowWeights(String) parameter #0:
@@ -1089,6 +1073,8 @@
Missing nullability on parameter `context` in method `ImageFilterButton`
MissingNullability: androidx.constraintlayout.utils.widget.ImageFilterButton#ImageFilterButton(android.content.Context, android.util.AttributeSet, int) parameter #1:
Missing nullability on parameter `attrs` in method `ImageFilterButton`
+MissingNullability: androidx.constraintlayout.utils.widget.ImageFilterButton#draw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `draw`
MissingNullability: androidx.constraintlayout.utils.widget.ImageFilterButton#setImageDrawable(android.graphics.drawable.Drawable) parameter #0:
Missing nullability on parameter `drawable` in method `setImageDrawable`
MissingNullability: androidx.constraintlayout.utils.widget.ImageFilterView#ImageFilterView(android.content.Context) parameter #0:
@@ -1101,6 +1087,8 @@
Missing nullability on parameter `context` in method `ImageFilterView`
MissingNullability: androidx.constraintlayout.utils.widget.ImageFilterView#ImageFilterView(android.content.Context, android.util.AttributeSet, int) parameter #1:
Missing nullability on parameter `attrs` in method `ImageFilterView`
+MissingNullability: androidx.constraintlayout.utils.widget.ImageFilterView#draw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `draw`
MissingNullability: androidx.constraintlayout.utils.widget.ImageFilterView#setAltImageDrawable(android.graphics.drawable.Drawable) parameter #0:
Missing nullability on parameter `altDrawable` in method `setAltImageDrawable`
MissingNullability: androidx.constraintlayout.utils.widget.ImageFilterView#setImageDrawable(android.graphics.drawable.Drawable) parameter #0:
@@ -1117,6 +1105,8 @@
Missing nullability on parameter `attrs` in method `MockView`
MissingNullability: androidx.constraintlayout.utils.widget.MockView#mText:
Missing nullability on field `mText` in class `class androidx.constraintlayout.utils.widget.MockView`
+MissingNullability: androidx.constraintlayout.utils.widget.MockView#onDraw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `onDraw`
MissingNullability: androidx.constraintlayout.utils.widget.MotionButton#MotionButton(android.content.Context) parameter #0:
Missing nullability on parameter `context` in method `MotionButton`
MissingNullability: androidx.constraintlayout.utils.widget.MotionButton#MotionButton(android.content.Context, android.util.AttributeSet) parameter #0:
@@ -1127,6 +1117,8 @@
Missing nullability on parameter `context` in method `MotionButton`
MissingNullability: androidx.constraintlayout.utils.widget.MotionButton#MotionButton(android.content.Context, android.util.AttributeSet, int) parameter #1:
Missing nullability on parameter `attrs` in method `MotionButton`
+MissingNullability: androidx.constraintlayout.utils.widget.MotionButton#draw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `draw`
MissingNullability: androidx.constraintlayout.utils.widget.MotionLabel#MotionLabel(android.content.Context) parameter #0:
Missing nullability on parameter `context` in method `MotionLabel`
MissingNullability: androidx.constraintlayout.utils.widget.MotionLabel#MotionLabel(android.content.Context, android.util.AttributeSet) parameter #0:
@@ -1135,6 +1127,8 @@
Missing nullability on parameter `context` in method `MotionLabel`
MissingNullability: androidx.constraintlayout.utils.widget.MotionLabel#getTypeface():
Missing nullability on method `getTypeface` return
+MissingNullability: androidx.constraintlayout.utils.widget.MotionLabel#onDraw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `onDraw`
MissingNullability: androidx.constraintlayout.utils.widget.MotionLabel#setText(CharSequence) parameter #0:
Missing nullability on parameter `text` in method `setText`
MissingNullability: androidx.constraintlayout.utils.widget.MotionLabel#setTypeface(android.graphics.Typeface) parameter #0:
@@ -1149,6 +1143,8 @@
Missing nullability on parameter `context` in method `MotionTelltales`
MissingNullability: androidx.constraintlayout.utils.widget.MotionTelltales#MotionTelltales(android.content.Context, android.util.AttributeSet, int) parameter #1:
Missing nullability on parameter `attrs` in method `MotionTelltales`
+MissingNullability: androidx.constraintlayout.utils.widget.MotionTelltales#onDraw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `onDraw`
MissingNullability: androidx.constraintlayout.utils.widget.MotionTelltales#setText(CharSequence) parameter #0:
Missing nullability on parameter `text` in method `setText`
MissingNullability: androidx.constraintlayout.widget.Barrier#Barrier(android.content.Context) parameter #0:
@@ -1267,6 +1263,8 @@
Missing nullability on field `mReferenceTags` in class `class androidx.constraintlayout.widget.ConstraintHelper`
MissingNullability: androidx.constraintlayout.widget.ConstraintHelper#myContext:
Missing nullability on field `myContext` in class `class androidx.constraintlayout.widget.ConstraintHelper`
+MissingNullability: androidx.constraintlayout.widget.ConstraintHelper#onDraw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `onDraw`
MissingNullability: androidx.constraintlayout.widget.ConstraintHelper#removeView(android.view.View) parameter #0:
Missing nullability on parameter `view` in method `removeView`
MissingNullability: androidx.constraintlayout.widget.ConstraintHelper#resolveRtl(androidx.constraintlayout.core.widgets.ConstraintWidget, boolean) parameter #0:
@@ -1713,6 +1711,8 @@
Missing nullability on parameter `context` in method `Guideline`
MissingNullability: androidx.constraintlayout.widget.Guideline#Guideline(android.content.Context, android.util.AttributeSet, int, int) parameter #1:
Missing nullability on parameter `attrs` in method `Guideline`
+MissingNullability: androidx.constraintlayout.widget.Guideline#draw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `draw`
MissingNullability: androidx.constraintlayout.widget.Placeholder#Placeholder(android.content.Context) parameter #0:
Missing nullability on parameter `context` in method `Placeholder`
MissingNullability: androidx.constraintlayout.widget.Placeholder#Placeholder(android.content.Context, android.util.AttributeSet) parameter #0:
@@ -1729,6 +1729,8 @@
Missing nullability on parameter `attrs` in method `Placeholder`
MissingNullability: androidx.constraintlayout.widget.Placeholder#getContent():
Missing nullability on method `getContent` return
+MissingNullability: androidx.constraintlayout.widget.Placeholder#onDraw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `onDraw`
MissingNullability: androidx.constraintlayout.widget.Placeholder#updatePostMeasure(androidx.constraintlayout.widget.ConstraintLayout) parameter #0:
Missing nullability on parameter `container` in method `updatePostMeasure`
MissingNullability: androidx.constraintlayout.widget.Placeholder#updatePreLayout(androidx.constraintlayout.widget.ConstraintLayout) parameter #0:
@@ -1747,6 +1749,8 @@
Missing nullability on parameter `context` in method `ReactiveGuide`
MissingNullability: androidx.constraintlayout.widget.ReactiveGuide#ReactiveGuide(android.content.Context, android.util.AttributeSet, int, int) parameter #1:
Missing nullability on parameter `attrs` in method `ReactiveGuide`
+MissingNullability: androidx.constraintlayout.widget.ReactiveGuide#draw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `draw`
MissingNullability: androidx.constraintlayout.widget.SharedValues#addListener(int, androidx.constraintlayout.widget.SharedValues.SharedValuesListener) parameter #1:
Missing nullability on parameter `listener` in method `addListener`
MissingNullability: androidx.constraintlayout.widget.SharedValues#removeListener(androidx.constraintlayout.widget.SharedValues.SharedValuesListener) parameter #0:
diff --git a/constraintlayout/constraintlayout/build.gradle b/constraintlayout/constraintlayout/build.gradle
index 060d756..52a3d6e 100644
--- a/constraintlayout/constraintlayout/build.gradle
+++ b/constraintlayout/constraintlayout/build.gradle
@@ -54,7 +54,7 @@
}
androidx {
- name = "Android ConstraintLayout Library"
+ name = "ConstraintLayout"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.CONSTRAINTLAYOUT
inceptionYear = "2022"
diff --git a/constraintlayout/constraintlayout/lint-baseline.xml b/constraintlayout/constraintlayout/lint-baseline.xml
index 6f8af38..a9b5204 100644
--- a/constraintlayout/constraintlayout/lint-baseline.xml
+++ b/constraintlayout/constraintlayout/lint-baseline.xml
@@ -5332,6 +5332,15 @@
<issue
id="UnknownNullness"
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void dispatchDraw(Canvas canvas) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
errorLine1=" public void setOnConstraintsChanged(ConstraintsChangedListener constraintsChangedListener) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -9247,6 +9256,15 @@
<issue
id="UnknownNullness"
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void dispatchDraw(Canvas canvas) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/constraintlayout/motion/widget/MotionLayout.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
errorLine1=" public void setScene(MotionScene scene) {"
errorLine2=" ~~~~~~~~~~~">
<location
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/Guideline.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/Guideline.java
index c4eda2a..577a8d7 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/Guideline.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/Guideline.java
@@ -61,29 +61,30 @@
* and {@link ConstraintSet#setGuidelinePercent} functions in {@link ConstraintSet}.
* <p>
* Example of a {@code Button} constrained to a vertical {@code Guideline}:
- * <pre>
- * <androidx.constraintlayout.widget.ConstraintLayout
- * xmlns:android="http://schemas.android.com/apk/res/android"
- * xmlns:app="http://schemas.android.com/apk/res-auto"
- * xmlns:tools="http://schemas.android.com/tools"
- * android:layout_width="match_parent"
- * android:layout_height="match_parent">
+ * <pre>{@code
+ * <androidx.constraintlayout.widget.ConstraintLayout
+ * xmlns:android="http://schemas.android.com/apk/res/android"
+ * xmlns:app="http://schemas.android.com/apk/res-auto"
+ * xmlns:tools="http://schemas.android.com/tools"
+ * android:layout_width="match_parent"
+ * android:layout_height="match_parent">
*
- * <androidx.constraintlayout.widget.Guideline
- * android:layout_width="wrap_content"
- * android:layout_height="wrap_content"
- * android:id="@+id/guideline"
- * app:layout_constraintGuide_begin="100dp"
- * android:orientation="vertical"/>
- * <Button
- * android:text="Button"
- * android:layout_width="wrap_content"
- * android:layout_height="wrap_content"
- * android:id="@+id/button"
- * app:layout_constraintLeft_toLeftOf="@+id/guideline"
- * android:layout_marginTop="16dp"
- * app:layout_constraintTop_toTopOf="parent" />
- * </androidx.constraintlayout.widget.ConstraintLayout>
+ * <androidx.constraintlayout.widget.Guideline
+ * android:layout_width="wrap_content"
+ * android:layout_height="wrap_content"
+ * android:id="@+id/guideline"
+ * app:layout_constraintGuide_begin="100dp"
+ * android:orientation="vertical"/>
+ * <Button
+ * android:text="Button"
+ * android:layout_width="wrap_content"
+ * android:layout_height="wrap_content"
+ * android:id="@+id/button"
+ * app:layout_constraintLeft_toLeftOf="@+id/guideline"
+ * android:layout_marginTop="16dp"
+ * app:layout_constraintTop_toTopOf="parent" />
+ * </androidx.constraintlayout.widget.ConstraintLayout>
+ * }
* </pre>
* <p/>
*/
diff --git a/contentpager/contentpager/build.gradle b/contentpager/contentpager/build.gradle
index d2624f7..81252bf 100644
--- a/contentpager/contentpager/build.gradle
+++ b/contentpager/contentpager/build.gradle
@@ -35,7 +35,7 @@
}
androidx {
- name = "Android Support Content"
+ name = "Content"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Library providing support for paging across content exposed via a ContentProvider. Use of this library allows a client to avoid expensive interprocess \"cursor window swaps\" on the UI thread."
diff --git a/coordinatorlayout/coordinatorlayout/api/api_lint.ignore b/coordinatorlayout/coordinatorlayout/api/api_lint.ignore
index f200680..06d3c6e 100644
--- a/coordinatorlayout/coordinatorlayout/api/api_lint.ignore
+++ b/coordinatorlayout/coordinatorlayout/api/api_lint.ignore
@@ -1,6 +1,4 @@
// Baseline format: 1.0
-InvalidNullabilityOverride: androidx.coordinatorlayout.widget.CoordinatorLayout#onDraw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `c` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
InvalidNullabilityOverride: androidx.coordinatorlayout.widget.CoordinatorLayout#onNestedPreScroll(android.view.View, int, int, int[]) parameter #0:
Invalid nullability on parameter `target` in method `onNestedPreScroll`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
InvalidNullabilityOverride: androidx.coordinatorlayout.widget.CoordinatorLayout#onNestedPreScroll(android.view.View, int, int, int[]) parameter #3:
@@ -35,6 +33,8 @@
Missing nullability on method `generateLayoutParams` return
MissingNullability: androidx.coordinatorlayout.widget.CoordinatorLayout#generateLayoutParams(android.view.ViewGroup.LayoutParams) parameter #0:
Missing nullability on parameter `p` in method `generateLayoutParams`
+MissingNullability: androidx.coordinatorlayout.widget.CoordinatorLayout#onDraw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `c` in method `onDraw`
MissingNullability: androidx.coordinatorlayout.widget.CoordinatorLayout#onInterceptTouchEvent(android.view.MotionEvent) parameter #0:
Missing nullability on parameter `ev` in method `onInterceptTouchEvent`
MissingNullability: androidx.coordinatorlayout.widget.CoordinatorLayout#onNestedFling(android.view.View, float, float, boolean) parameter #0:
diff --git a/coordinatorlayout/coordinatorlayout/build.gradle b/coordinatorlayout/coordinatorlayout/build.gradle
index bc110f8..9578993 100644
--- a/coordinatorlayout/coordinatorlayout/build.gradle
+++ b/coordinatorlayout/coordinatorlayout/build.gradle
@@ -48,7 +48,7 @@
}
androidx {
- name = "Android Support Library Coordinator Layout"
+ name = "Coordinator Layout"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2011"
description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/coordinatorlayout/coordinatorlayout/lint-baseline.xml b/coordinatorlayout/coordinatorlayout/lint-baseline.xml
index eb4748e..b3144af 100644
--- a/coordinatorlayout/coordinatorlayout/lint-baseline.xml
+++ b/coordinatorlayout/coordinatorlayout/lint-baseline.xml
@@ -1,5 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.4.0-alpha08" type="baseline" client="gradle" dependencies="false" name="AGP (7.4.0-alpha08)" variant="all" version="7.4.0-alpha08">
+<issues format="6" by="lint 8.0.0-alpha07" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-alpha07)" variant="all" version="8.0.0-alpha07">
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected boolean drawChild(Canvas canvas, View child, long drawingTime) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java"/>
+ </issue>
<issue
id="UnknownNullness"
diff --git a/core/core-animation-testing/build.gradle b/core/core-animation-testing/build.gradle
index 046bc43..78bb2e4 100644
--- a/core/core-animation-testing/build.gradle
+++ b/core/core-animation-testing/build.gradle
@@ -29,7 +29,7 @@
}
androidx {
- name = "Android Support Animator Testing"
+ name = "Animation Testing"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.CORE_ANIMATION_TESTING
inceptionYear = "2018"
diff --git a/core/core-animation/build.gradle b/core/core-animation/build.gradle
index fb7cc95..940504b 100644
--- a/core/core-animation/build.gradle
+++ b/core/core-animation/build.gradle
@@ -32,7 +32,7 @@
}
androidx {
- name = "Android Support Animation"
+ name = "Animation"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.CORE_ANIMATION
inceptionYear = "2018"
diff --git a/core/core-appdigest/build.gradle b/core/core-appdigest/build.gradle
index a0d4af2..7dadc10 100644
--- a/core/core-appdigest/build.gradle
+++ b/core/core-appdigest/build.gradle
@@ -33,7 +33,7 @@
}
androidx {
- name = "AndroidX AppDigest Library"
+ name = "AppDigest"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.CORE_APPDIGEST
inceptionYear = "2020"
diff --git a/core/core-google-shortcuts/build.gradle b/core/core-google-shortcuts/build.gradle
index 3071a80..615be8f 100644
--- a/core/core-google-shortcuts/build.gradle
+++ b/core/core-google-shortcuts/build.gradle
@@ -54,7 +54,7 @@
}
androidx {
- name = "Google Shortcuts Integration Library"
+ name = "Google Shortcuts Integration"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.CORE_GOOGLE_SHORTCUTS
inceptionYear = "2021"
diff --git a/core/core-graphics-integration-tests/testapp/build.gradle b/core/core-graphics-integration-tests/testapp/build.gradle
index 59d5bb3..768d7b4 100644
--- a/core/core-graphics-integration-tests/testapp/build.gradle
+++ b/core/core-graphics-integration-tests/testapp/build.gradle
@@ -40,7 +40,7 @@
}
androidx {
- name = "AndroidX bitmap scaling Sample"
+ name = "bitmap scaling Sample"
inceptionYear = "2021"
description = "Sample for the AndoridX graphics bitmap compatibility"
}
diff --git a/core/core-ktx/src/main/java/androidx/core/os/OutcomeReceiver.kt b/core/core-ktx/src/main/java/androidx/core/os/OutcomeReceiver.kt
index 74052a3..d6698aa 100644
--- a/core/core-ktx/src/main/java/androidx/core/os/OutcomeReceiver.kt
+++ b/core/core-ktx/src/main/java/androidx/core/os/OutcomeReceiver.kt
@@ -61,7 +61,7 @@
private class ContinuationOutcomeReceiver<R, E : Throwable>(
private val continuation: Continuation<R>
) : OutcomeReceiver<R, E>, AtomicBoolean(false) {
- override fun onResult(result: R & Any) {
+ override fun onResult(result: R) {
// Do not attempt to resume more than once, even if the caller of the returned
// OutcomeReceiver is buggy and tries anyway.
if (compareAndSet(false, true)) {
diff --git a/core/core-location-altitude/build.gradle b/core/core-location-altitude/build.gradle
index 6f5e108..ccee9b7 100644
--- a/core/core-location-altitude/build.gradle
+++ b/core/core-location-altitude/build.gradle
@@ -41,7 +41,7 @@
}
androidx {
- name = "Location Altitude Compatibility Library"
+ name = "Location Altitude"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.CORE_LOCATION_ALTITUDE
inceptionYear = "2022"
diff --git a/core/core-performance/src/test/resources/robolectric.properties b/core/core-performance/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..69fde47
--- /dev/null
+++ b/core/core-performance/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/core/core-remoteviews/build.gradle b/core/core-remoteviews/build.gradle
index eb33cc1..78adb5b 100644
--- a/core/core-remoteviews/build.gradle
+++ b/core/core-remoteviews/build.gradle
@@ -58,7 +58,7 @@
}
androidx {
- name = "AndroidX RemoteViews Support"
+ name = "RemoteViews"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.CORE_REMOTEVIEWS
inceptionYear = "2021"
diff --git a/core/core-role/build.gradle b/core/core-role/build.gradle
index e2f2c0a..f820a13 100644
--- a/core/core-role/build.gradle
+++ b/core/core-role/build.gradle
@@ -14,7 +14,7 @@
}
androidx {
- name = "Android Support Library Role"
+ name = "Role"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.CORE_ROLE
inceptionYear = "2019"
diff --git a/core/core-splashscreen/samples/build.gradle b/core/core-splashscreen/samples/build.gradle
index 3c59a31..ae1d810 100644
--- a/core/core-splashscreen/samples/build.gradle
+++ b/core/core-splashscreen/samples/build.gradle
@@ -42,7 +42,7 @@
}
androidx {
- name = "AndroidX Splashscreen Samples"
+ name = "Splashscreen Samples"
type = LibraryType.SAMPLES
mavenVersion = LibraryVersions.CORE_SPLASHSCREEN
inceptionYear = "2021"
diff --git a/core/core-telecom/OWNERS b/core/core-telecom/OWNERS
new file mode 100644
index 0000000..7de7eb4
--- /dev/null
+++ b/core/core-telecom/OWNERS
@@ -0,0 +1,9 @@
+# Bug component: 151185
+breadley@google.com
+tgunn@google.com
+xiaotonj@google.com
+chinmayd@google.com
+tjstuart@google.com
+rgreenwalt@google.com
+pmadapurmath@google.com
+grantmenke@google.com
diff --git a/core/core-telecom/api/current.txt b/core/core-telecom/api/current.txt
new file mode 100644
index 0000000..aa73740
--- /dev/null
+++ b/core/core-telecom/api/current.txt
@@ -0,0 +1,98 @@
+// Signature format: 4.0
+package androidx.core.telecom {
+
+ public final class CallAttributesCompat {
+ ctor public CallAttributesCompat(CharSequence displayName, android.net.Uri address, int direction, optional int callType, optional int callCapabilities);
+ method public android.net.Uri getAddress();
+ method public int getCallCapabilities();
+ method public int getCallType();
+ method public int getDirection();
+ method public CharSequence getDisplayName();
+ property public final android.net.Uri address;
+ property public final int callCapabilities;
+ property public final int callType;
+ property public final int direction;
+ property public final CharSequence displayName;
+ field public static final int CALL_TYPE_AUDIO_CALL = 1; // 0x1
+ field public static final int CALL_TYPE_VIDEO_CALL = 2; // 0x2
+ field public static final androidx.core.telecom.CallAttributesCompat.Companion Companion;
+ field public static final int DIRECTION_INCOMING = 1; // 0x1
+ field public static final int DIRECTION_OUTGOING = 2; // 0x2
+ field public static final int SUPPORTS_SET_INACTIVE = 2; // 0x2
+ field public static final int SUPPORTS_STREAM = 4; // 0x4
+ field public static final int SUPPORTS_TRANSFER = 8; // 0x8
+ }
+
+ public static final class CallAttributesCompat.Companion {
+ }
+
+ public interface CallControlCallback {
+ method public suspend Object? onAnswer(int callType, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? onDisconnect(android.telecom.DisconnectCause disconnectCause, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? onSetActive(kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? onSetInactive(kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ }
+
+ public interface CallControlScope {
+ method public suspend Object? answer(int callType, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? disconnect(android.telecom.DisconnectCause disconnectCause, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public kotlinx.coroutines.flow.Flow<java.util.List<androidx.core.telecom.CallEndpointCompat>> getAvailableEndpoints();
+ method public android.os.ParcelUuid getCallId();
+ method public kotlinx.coroutines.flow.Flow<androidx.core.telecom.CallEndpointCompat> getCurrentCallEndpoint();
+ method public kotlinx.coroutines.flow.Flow<java.lang.Boolean> isMuted();
+ method public suspend Object? requestEndpointChange(androidx.core.telecom.CallEndpointCompat endpoint, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? setActive(kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public void setCallback(androidx.core.telecom.CallControlCallback callControlCallback);
+ method public suspend Object? setInactive(kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ property public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.core.telecom.CallEndpointCompat>> availableEndpoints;
+ property public abstract kotlinx.coroutines.flow.Flow<androidx.core.telecom.CallEndpointCompat> currentCallEndpoint;
+ property public abstract kotlinx.coroutines.flow.Flow<java.lang.Boolean> isMuted;
+ }
+
+ @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CallEndpointCompat {
+ ctor public CallEndpointCompat(CharSequence name, int type, android.os.ParcelUuid identifier);
+ method public android.os.ParcelUuid getIdentifier();
+ method public CharSequence getName();
+ method public int getType();
+ property public final android.os.ParcelUuid identifier;
+ property public final CharSequence name;
+ property public final int type;
+ field public static final androidx.core.telecom.CallEndpointCompat.Companion Companion;
+ field public static final int TYPE_BLUETOOTH = 2; // 0x2
+ field public static final int TYPE_EARPIECE = 1; // 0x1
+ field public static final int TYPE_SPEAKER = 4; // 0x4
+ field public static final int TYPE_STREAMING = 5; // 0x5
+ field public static final int TYPE_UNKNOWN = -1; // 0xffffffff
+ field public static final int TYPE_WIRED_HEADSET = 3; // 0x3
+ }
+
+ public static final class CallEndpointCompat.Companion {
+ }
+
+ public final class CallException extends java.lang.RuntimeException {
+ ctor public CallException(optional int code, optional String? message);
+ method public int getCode();
+ property public final int code;
+ field public static final androidx.core.telecom.CallException.Companion Companion;
+ field public static final int ERROR_CALLBACKS_CODE = 2; // 0x2
+ field public static final int ERROR_UNKNOWN_CODE = 1; // 0x1
+ }
+
+ public static final class CallException.Companion {
+ }
+
+ @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CallsManager {
+ ctor public CallsManager(android.content.Context context);
+ method @RequiresPermission("android.permission.MANAGE_OWN_CALLS") public suspend Object? addCall(androidx.core.telecom.CallAttributesCompat callAttributes, kotlin.jvm.functions.Function1<? super androidx.core.telecom.CallControlScope,kotlin.Unit> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method @RequiresPermission("android.permission.MANAGE_OWN_CALLS") public void registerAppWithTelecom(int capabilities);
+ field public static final int CAPABILITY_BASELINE = 1; // 0x1
+ field public static final int CAPABILITY_SUPPORTS_CALL_STREAMING = 4; // 0x4
+ field public static final int CAPABILITY_SUPPORTS_VIDEO_CALLING = 2; // 0x2
+ field public static final androidx.core.telecom.CallsManager.Companion Companion;
+ }
+
+ public static final class CallsManager.Companion {
+ }
+
+}
+
diff --git a/core/core-telecom/api/public_plus_experimental_current.txt b/core/core-telecom/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..aa73740
--- /dev/null
+++ b/core/core-telecom/api/public_plus_experimental_current.txt
@@ -0,0 +1,98 @@
+// Signature format: 4.0
+package androidx.core.telecom {
+
+ public final class CallAttributesCompat {
+ ctor public CallAttributesCompat(CharSequence displayName, android.net.Uri address, int direction, optional int callType, optional int callCapabilities);
+ method public android.net.Uri getAddress();
+ method public int getCallCapabilities();
+ method public int getCallType();
+ method public int getDirection();
+ method public CharSequence getDisplayName();
+ property public final android.net.Uri address;
+ property public final int callCapabilities;
+ property public final int callType;
+ property public final int direction;
+ property public final CharSequence displayName;
+ field public static final int CALL_TYPE_AUDIO_CALL = 1; // 0x1
+ field public static final int CALL_TYPE_VIDEO_CALL = 2; // 0x2
+ field public static final androidx.core.telecom.CallAttributesCompat.Companion Companion;
+ field public static final int DIRECTION_INCOMING = 1; // 0x1
+ field public static final int DIRECTION_OUTGOING = 2; // 0x2
+ field public static final int SUPPORTS_SET_INACTIVE = 2; // 0x2
+ field public static final int SUPPORTS_STREAM = 4; // 0x4
+ field public static final int SUPPORTS_TRANSFER = 8; // 0x8
+ }
+
+ public static final class CallAttributesCompat.Companion {
+ }
+
+ public interface CallControlCallback {
+ method public suspend Object? onAnswer(int callType, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? onDisconnect(android.telecom.DisconnectCause disconnectCause, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? onSetActive(kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? onSetInactive(kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ }
+
+ public interface CallControlScope {
+ method public suspend Object? answer(int callType, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? disconnect(android.telecom.DisconnectCause disconnectCause, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public kotlinx.coroutines.flow.Flow<java.util.List<androidx.core.telecom.CallEndpointCompat>> getAvailableEndpoints();
+ method public android.os.ParcelUuid getCallId();
+ method public kotlinx.coroutines.flow.Flow<androidx.core.telecom.CallEndpointCompat> getCurrentCallEndpoint();
+ method public kotlinx.coroutines.flow.Flow<java.lang.Boolean> isMuted();
+ method public suspend Object? requestEndpointChange(androidx.core.telecom.CallEndpointCompat endpoint, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? setActive(kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public void setCallback(androidx.core.telecom.CallControlCallback callControlCallback);
+ method public suspend Object? setInactive(kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ property public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.core.telecom.CallEndpointCompat>> availableEndpoints;
+ property public abstract kotlinx.coroutines.flow.Flow<androidx.core.telecom.CallEndpointCompat> currentCallEndpoint;
+ property public abstract kotlinx.coroutines.flow.Flow<java.lang.Boolean> isMuted;
+ }
+
+ @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CallEndpointCompat {
+ ctor public CallEndpointCompat(CharSequence name, int type, android.os.ParcelUuid identifier);
+ method public android.os.ParcelUuid getIdentifier();
+ method public CharSequence getName();
+ method public int getType();
+ property public final android.os.ParcelUuid identifier;
+ property public final CharSequence name;
+ property public final int type;
+ field public static final androidx.core.telecom.CallEndpointCompat.Companion Companion;
+ field public static final int TYPE_BLUETOOTH = 2; // 0x2
+ field public static final int TYPE_EARPIECE = 1; // 0x1
+ field public static final int TYPE_SPEAKER = 4; // 0x4
+ field public static final int TYPE_STREAMING = 5; // 0x5
+ field public static final int TYPE_UNKNOWN = -1; // 0xffffffff
+ field public static final int TYPE_WIRED_HEADSET = 3; // 0x3
+ }
+
+ public static final class CallEndpointCompat.Companion {
+ }
+
+ public final class CallException extends java.lang.RuntimeException {
+ ctor public CallException(optional int code, optional String? message);
+ method public int getCode();
+ property public final int code;
+ field public static final androidx.core.telecom.CallException.Companion Companion;
+ field public static final int ERROR_CALLBACKS_CODE = 2; // 0x2
+ field public static final int ERROR_UNKNOWN_CODE = 1; // 0x1
+ }
+
+ public static final class CallException.Companion {
+ }
+
+ @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CallsManager {
+ ctor public CallsManager(android.content.Context context);
+ method @RequiresPermission("android.permission.MANAGE_OWN_CALLS") public suspend Object? addCall(androidx.core.telecom.CallAttributesCompat callAttributes, kotlin.jvm.functions.Function1<? super androidx.core.telecom.CallControlScope,kotlin.Unit> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method @RequiresPermission("android.permission.MANAGE_OWN_CALLS") public void registerAppWithTelecom(int capabilities);
+ field public static final int CAPABILITY_BASELINE = 1; // 0x1
+ field public static final int CAPABILITY_SUPPORTS_CALL_STREAMING = 4; // 0x4
+ field public static final int CAPABILITY_SUPPORTS_VIDEO_CALLING = 2; // 0x2
+ field public static final androidx.core.telecom.CallsManager.Companion Companion;
+ }
+
+ public static final class CallsManager.Companion {
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/core/core-telecom/api/res-current.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to core/core-telecom/api/res-current.txt
diff --git a/core/core-telecom/api/restricted_current.txt b/core/core-telecom/api/restricted_current.txt
new file mode 100644
index 0000000..8c6ce9f
--- /dev/null
+++ b/core/core-telecom/api/restricted_current.txt
@@ -0,0 +1,116 @@
+// Signature format: 4.0
+package androidx.core.telecom {
+
+ public final class CallAttributesCompat {
+ ctor public CallAttributesCompat(CharSequence displayName, android.net.Uri address, @androidx.core.telecom.CallAttributesCompat.Companion.Direction int direction, optional @androidx.core.telecom.CallAttributesCompat.Companion.CallType int callType, optional @androidx.core.telecom.CallAttributesCompat.Companion.CallCapability int callCapabilities);
+ method public android.net.Uri getAddress();
+ method public int getCallCapabilities();
+ method public int getCallType();
+ method public int getDirection();
+ method public CharSequence getDisplayName();
+ property public final android.net.Uri address;
+ property public final int callCapabilities;
+ property public final int callType;
+ property public final int direction;
+ property public final CharSequence displayName;
+ field public static final int CALL_TYPE_AUDIO_CALL = 1; // 0x1
+ field public static final int CALL_TYPE_VIDEO_CALL = 2; // 0x2
+ field public static final androidx.core.telecom.CallAttributesCompat.Companion Companion;
+ field public static final int DIRECTION_INCOMING = 1; // 0x1
+ field public static final int DIRECTION_OUTGOING = 2; // 0x2
+ field public static final int SUPPORTS_SET_INACTIVE = 2; // 0x2
+ field public static final int SUPPORTS_STREAM = 4; // 0x4
+ field public static final int SUPPORTS_TRANSFER = 8; // 0x8
+ }
+
+ public static final class CallAttributesCompat.Companion {
+ }
+
+ @IntDef(value={androidx.core.telecom.CallAttributesCompat.SUPPORTS_SET_INACTIVE, androidx.core.telecom.CallAttributesCompat.SUPPORTS_STREAM, androidx.core.telecom.CallAttributesCompat.SUPPORTS_TRANSFER}, flag=true) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.TYPE, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface CallAttributesCompat.Companion.CallCapability {
+ }
+
+ @IntDef({androidx.core.telecom.CallAttributesCompat.CALL_TYPE_AUDIO_CALL, androidx.core.telecom.CallAttributesCompat.CALL_TYPE_VIDEO_CALL}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.TYPE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.PROPERTY}) public static @interface CallAttributesCompat.Companion.CallType {
+ }
+
+ @IntDef({androidx.core.telecom.CallAttributesCompat.DIRECTION_INCOMING, androidx.core.telecom.CallAttributesCompat.DIRECTION_OUTGOING}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.TYPE, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface CallAttributesCompat.Companion.Direction {
+ }
+
+ public interface CallControlCallback {
+ method public suspend Object? onAnswer(@androidx.core.telecom.CallAttributesCompat.Companion.CallType int callType, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? onDisconnect(android.telecom.DisconnectCause disconnectCause, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? onSetActive(kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? onSetInactive(kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ }
+
+ public interface CallControlScope {
+ method public suspend Object? answer(@androidx.core.telecom.CallAttributesCompat.Companion.CallType int callType, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? disconnect(android.telecom.DisconnectCause disconnectCause, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public kotlinx.coroutines.flow.Flow<java.util.List<androidx.core.telecom.CallEndpointCompat>> getAvailableEndpoints();
+ method public android.os.ParcelUuid getCallId();
+ method public kotlinx.coroutines.flow.Flow<androidx.core.telecom.CallEndpointCompat> getCurrentCallEndpoint();
+ method public kotlinx.coroutines.flow.Flow<java.lang.Boolean> isMuted();
+ method public suspend Object? requestEndpointChange(androidx.core.telecom.CallEndpointCompat endpoint, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public suspend Object? setActive(kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ method public void setCallback(androidx.core.telecom.CallControlCallback callControlCallback);
+ method public suspend Object? setInactive(kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ property public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.core.telecom.CallEndpointCompat>> availableEndpoints;
+ property public abstract kotlinx.coroutines.flow.Flow<androidx.core.telecom.CallEndpointCompat> currentCallEndpoint;
+ property public abstract kotlinx.coroutines.flow.Flow<java.lang.Boolean> isMuted;
+ }
+
+ @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CallEndpointCompat {
+ ctor public CallEndpointCompat(CharSequence name, int type, android.os.ParcelUuid identifier);
+ method public android.os.ParcelUuid getIdentifier();
+ method public CharSequence getName();
+ method public int getType();
+ property public final android.os.ParcelUuid identifier;
+ property public final CharSequence name;
+ property public final int type;
+ field public static final androidx.core.telecom.CallEndpointCompat.Companion Companion;
+ field public static final int TYPE_BLUETOOTH = 2; // 0x2
+ field public static final int TYPE_EARPIECE = 1; // 0x1
+ field public static final int TYPE_SPEAKER = 4; // 0x4
+ field public static final int TYPE_STREAMING = 5; // 0x5
+ field public static final int TYPE_UNKNOWN = -1; // 0xffffffff
+ field public static final int TYPE_WIRED_HEADSET = 3; // 0x3
+ }
+
+ public static final class CallEndpointCompat.Companion {
+ }
+
+ @IntDef({androidx.core.telecom.CallEndpointCompat.TYPE_UNKNOWN, androidx.core.telecom.CallEndpointCompat.TYPE_EARPIECE, androidx.core.telecom.CallEndpointCompat.TYPE_BLUETOOTH, androidx.core.telecom.CallEndpointCompat.TYPE_WIRED_HEADSET, androidx.core.telecom.CallEndpointCompat.TYPE_SPEAKER, androidx.core.telecom.CallEndpointCompat.TYPE_STREAMING}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.TYPE, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface CallEndpointCompat.Companion.EndpointType {
+ }
+
+ public final class CallException extends java.lang.RuntimeException {
+ ctor public CallException(optional @androidx.core.telecom.CallException.Companion.CallErrorCode int code, optional String? message);
+ method public int getCode();
+ property public final int code;
+ field public static final androidx.core.telecom.CallException.Companion Companion;
+ field public static final int ERROR_CALLBACKS_CODE = 2; // 0x2
+ field public static final int ERROR_UNKNOWN_CODE = 1; // 0x1
+ }
+
+ public static final class CallException.Companion {
+ }
+
+ @IntDef({androidx.core.telecom.CallException.ERROR_UNKNOWN_CODE, androidx.core.telecom.CallException.ERROR_CALLBACKS_CODE}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) public static @interface CallException.Companion.CallErrorCode {
+ }
+
+ @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CallsManager {
+ ctor public CallsManager(android.content.Context context);
+ method @RequiresPermission("android.permission.MANAGE_OWN_CALLS") public suspend Object? addCall(androidx.core.telecom.CallAttributesCompat callAttributes, kotlin.jvm.functions.Function1<? super androidx.core.telecom.CallControlScope,kotlin.Unit> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method @RequiresPermission("android.permission.MANAGE_OWN_CALLS") public void registerAppWithTelecom(@androidx.core.telecom.CallsManager.Companion.Capability int capabilities);
+ field public static final int CAPABILITY_BASELINE = 1; // 0x1
+ field public static final int CAPABILITY_SUPPORTS_CALL_STREAMING = 4; // 0x4
+ field public static final int CAPABILITY_SUPPORTS_VIDEO_CALLING = 2; // 0x2
+ field public static final androidx.core.telecom.CallsManager.Companion Companion;
+ }
+
+ public static final class CallsManager.Companion {
+ }
+
+ @IntDef(value={androidx.core.telecom.CallsManager.CAPABILITY_BASELINE, androidx.core.telecom.CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING, androidx.core.telecom.CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING}, flag=true) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.TYPE}) public static @interface CallsManager.Companion.Capability {
+ }
+
+}
+
diff --git a/core/core-telecom/build.gradle b/core/core-telecom/build.gradle
new file mode 100644
index 0000000..2e308881
--- /dev/null
+++ b/core/core-telecom/build.gradle
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ // core-telecom dependencies
+ api(libs.kotlinStdlib)
+ api(libs.guavaListenableFuture)
+ implementation("androidx.annotation:annotation:1.4.0")
+ implementation("androidx.core:core:1.9.0")
+ implementation(libs.kotlinCoroutinesCore)
+ implementation(libs.kotlinCoroutinesGuava)
+ // Test dependencies
+ androidTestImplementation(project(":internal-testutils-common"))
+ androidTestImplementation(libs.kotlinStdlib)
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(libs.espressoCore)
+ androidTestImplementation(libs.multidex)
+}
+
+android {
+ namespace "androidx.core.telecom"
+}
+
+androidx {
+ name = "androidx.core:core-telecom"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenVersion = LibraryVersions.CORE_TELECOM
+ inceptionYear = "2023"
+ description = "Integrate VoIP calls with the Telecom framework."
+}
diff --git a/core/core-telecom/integration-tests/testapp/build.gradle b/core/core-telecom/integration-tests/testapp/build.gradle
new file mode 100644
index 0000000..582f731
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/build.gradle
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 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.
+ */
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.application")
+ id("kotlin-android")
+}
+
+android {
+ namespace 'androidx.core.telecom.test'
+
+ defaultConfig {
+ applicationId "androidx.core.telecom.test"
+ minSdk 21
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ buildFeatures {
+ viewBinding true
+ }
+}
+
+dependencies {
+ implementation(libs.constraintLayout)
+ implementation("androidx.annotation:annotation:1.4.0")
+ implementation("androidx.core:core:1.9.0")
+ implementation(project(":core:core-telecom"))
+ implementation('androidx.appcompat:appcompat:1.6.1')
+ implementation('androidx.navigation:navigation-fragment-ktx:2.5.3')
+ implementation('androidx.navigation:navigation-ui-ktx:2.5.3')
+ implementation('androidx.recyclerview:recyclerview:1.2.1')
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testRunner)
+}
+
diff --git a/core/core-telecom/integration-tests/testapp/src/main/AndroidManifest.xml b/core/core-telecom/integration-tests/testapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..529aa31
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
+
+ <application
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/Theme.AppCompat">
+ <activity
+ android:name=".CallingMainActivity"
+ android:exported="true"
+ android:label="@string/main_activity_name"
+ android:theme="@style/Theme.AppCompat">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallListAdapter.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallListAdapter.kt
new file mode 100644
index 0000000..7c33fc8
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallListAdapter.kt
@@ -0,0 +1,165 @@
+/*
+ * 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.core.telecom.test
+
+import android.telecom.CallEndpoint
+import android.telecom.DisconnectCause
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.TextView
+import androidx.annotation.RequiresApi
+import androidx.recyclerview.widget.RecyclerView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+@RequiresApi(34)
+class CallListAdapter(private var mList: ArrayList<CallRow>?) :
+ RecyclerView.Adapter<CallListAdapter.ViewHolder>() {
+
+ var mCallIdToViewHolder: MutableMap<String, ViewHolder> = mutableMapOf()
+
+ class ViewHolder(ItemView: View) : RecyclerView.ViewHolder(ItemView) {
+ // TextViews
+ val callCount: TextView = itemView.findViewById(R.id.callNumber)
+ val callIdTextView: TextView = itemView.findViewById(R.id.callIdTextView)
+ val currentState: TextView = itemView.findViewById(R.id.callStateTextView)
+ val currentEndpoint: TextView = itemView.findViewById(R.id.endpointStateTextView)
+
+ // Call State Buttons
+ val activeButton: Button = itemView.findViewById(R.id.activeButton)
+ val holdButton: Button = itemView.findViewById(R.id.holdButton)
+ val disconnectButton: Button = itemView.findViewById(R.id.disconnectButton)
+
+ // Call Audio Buttons
+ val earpieceButton: Button = itemView.findViewById(R.id.earpieceButton)
+ val speakerButton: Button = itemView.findViewById(R.id.speakerButton)
+ val bluetoothButton: Button = itemView.findViewById(R.id.bluetoothButton)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ // inflates the card_view_design view that is used to hold list item
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.call_row, parent, false)
+
+ return ViewHolder(view)
+ }
+
+ override fun getItemCount(): Int {
+ return mList?.size ?: 0
+ }
+
+ // Set the data for the user
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val ItemsViewModel = mList?.get(position)
+
+ // sets the text to the textview from our itemHolder class
+ if (ItemsViewModel != null) {
+ mCallIdToViewHolder[ItemsViewModel.callObject.mTelecomCallId] = holder
+
+ holder.callCount.text = "Call # " + ItemsViewModel.callNumber.toString() + "; "
+ holder.callIdTextView.text = "ID=[" + ItemsViewModel.callObject.mTelecomCallId + "]"
+
+ holder.activeButton.setOnClickListener {
+ CoroutineScope(Dispatchers.Main).launch {
+ if (ItemsViewModel.callObject.mCallControl!!.setActive()) {
+ holder.currentState.text = "CurrentState=[active]"
+ }
+ }
+ }
+
+ holder.holdButton.setOnClickListener {
+ CoroutineScope(Dispatchers.Main).launch {
+ if (ItemsViewModel.callObject.mCallControl!!.setInactive()) {
+ holder.currentState.text = "CurrentState=[onHold]"
+ }
+ }
+ }
+
+ holder.disconnectButton.setOnClickListener {
+ CoroutineScope(Dispatchers.IO).launch {
+ ItemsViewModel.callObject.mCallControl?.disconnect(
+ DisconnectCause(
+ DisconnectCause.LOCAL
+ )
+ )
+ }
+ holder.currentState.text = "CurrentState=[null]"
+ mList?.remove(ItemsViewModel)
+ this.notifyDataSetChanged()
+ }
+
+ holder.earpieceButton.setOnClickListener {
+ CoroutineScope(Dispatchers.Main).launch {
+ val earpieceEndpoint =
+ ItemsViewModel.callObject.getEndpointType(CallEndpoint.TYPE_EARPIECE)
+ if (earpieceEndpoint != null) {
+ ItemsViewModel.callObject.mCallControl?.requestEndpointChange(
+ earpieceEndpoint
+ )
+ }
+ }
+ }
+ holder.speakerButton.setOnClickListener {
+ CoroutineScope(Dispatchers.Main).launch {
+ val speakerEndpoint = ItemsViewModel.callObject
+ .getEndpointType(CallEndpoint.TYPE_SPEAKER)
+ if (speakerEndpoint != null) {
+ val success = ItemsViewModel.callObject.mCallControl?.requestEndpointChange(
+ speakerEndpoint
+ )
+ if (success == true) {
+ holder.currentEndpoint.text = "currentEndpoint=[speaker]"
+ }
+ }
+ }
+ }
+
+ holder.bluetoothButton.setOnClickListener {
+ CoroutineScope(Dispatchers.Main).launch {
+ val bluetoothEndpoint = ItemsViewModel.callObject
+ .getEndpointType(CallEndpoint.TYPE_BLUETOOTH)
+ if (bluetoothEndpoint != null) {
+ val success = ItemsViewModel.callObject.mCallControl?.requestEndpointChange(
+ bluetoothEndpoint
+ )
+ if (success == true) {
+ holder.currentEndpoint.text =
+ "currentEndpoint=[BT:${bluetoothEndpoint.name}]"
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fun updateCallState(callId: String, state: String) {
+ CoroutineScope(Dispatchers.Main).launch {
+ val holder = mCallIdToViewHolder[callId]
+ holder?.callIdTextView?.text = "currentState=[$state]"
+ }
+ }
+
+ fun updateEndpoint(callId: String, endpoint: String) {
+ CoroutineScope(Dispatchers.Main).launch {
+ val holder = mCallIdToViewHolder[callId]
+ holder?.currentEndpoint?.text = "currentEndpoint=[$endpoint]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallRow.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallRow.kt
new file mode 100644
index 0000000..484a17e
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallRow.kt
@@ -0,0 +1,19 @@
+/*
+ * 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.core.telecom.test
+
+data class CallRow(val callNumber: Int, val callObject: VoipCall)
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt
new file mode 100644
index 0000000..f1cebe4
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt
@@ -0,0 +1,164 @@
+/*
+ * 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.core.telecom.test
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.os.Bundle
+import android.telecom.DisconnectCause
+import android.util.Log
+import android.widget.Button
+import android.widget.CheckBox
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallAttributesCompat
+import androidx.core.telecom.CallsManager
+import androidx.core.view.WindowCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+@RequiresApi(34)
+class CallingMainActivity : Activity() {
+ // Activity
+ private val TAG = CallingMainActivity::class.simpleName
+ private val mScope = CoroutineScope(Dispatchers.Default)
+ private var mCallCount: Int = 0
+
+ // Telecom
+ private var mCallsManager: CallsManager? = null
+
+ // Call Log objects
+ private var mRecyclerView: RecyclerView? = null
+ private var mCallObjects: ArrayList<CallRow> = ArrayList()
+ private var mAdapter: CallListAdapter? = CallListAdapter(mCallObjects)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+
+ setContentView(R.layout.activity_main)
+
+ mCallsManager = CallsManager(this)
+ mCallCount = 0
+
+ val registerPhoneAccountButton = findViewById<Button>(R.id.registerButton)
+ registerPhoneAccountButton.setOnClickListener {
+ mScope.launch {
+ registerPhoneAccount()
+ }
+ }
+
+ val addOutgoingCallButton = findViewById<Button>(R.id.addOutgoingCall)
+ addOutgoingCallButton.setOnClickListener {
+ mScope.launch {
+ addCallWithAttributes(Utilities.OUTGOING_CALL_ATTRIBUTES)
+ }
+ }
+
+ val addIncomingCallButton = findViewById<Button>(R.id.addIncomingCall)
+ addIncomingCallButton.setOnClickListener {
+ mScope.launch {
+ addCallWithAttributes(Utilities.INCOMING_CALL_ATTRIBUTES)
+ }
+ }
+
+ // set up the call list view holder
+ mRecyclerView = findViewById(R.id.callListRecyclerView)
+ mRecyclerView?.layoutManager = LinearLayoutManager(this)
+ mRecyclerView?.adapter = mAdapter
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ for (call in mCallObjects) {
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ call.callObject.mCallControl?.disconnect(DisconnectCause(DisconnectCause.LOCAL))
+ } catch (e: Exception) {
+ Log.i(TAG, "onDestroy: exception hit trying to destroy")
+ }
+ }
+ }
+ }
+
+ @SuppressLint("WrongConstant")
+ private fun registerPhoneAccount() {
+ var capabilities: @CallsManager.Companion.Capability Int = CallsManager.CAPABILITY_BASELINE
+
+ val videoCallingCheckBox = findViewById<CheckBox>(R.id.VideoCallingCheckBox)
+ if (videoCallingCheckBox.isChecked) {
+ capabilities = capabilities or CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING
+ }
+ val streamingCheckBox = findViewById<CheckBox>(R.id.streamingCheckBox)
+ if (streamingCheckBox.isChecked) {
+ capabilities = capabilities or CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING
+ }
+ mCallsManager?.registerAppWithTelecom(capabilities)
+ }
+
+ private suspend fun addCallWithAttributes(attributes: CallAttributesCompat) {
+ Log.i(TAG, "addCallWithAttributes: attributes=$attributes")
+ val callObject = VoipCall()
+
+ CoroutineScope(Dispatchers.IO).launch {
+ val coroutineScope = this
+ try {
+ mCallsManager!!.addCall(attributes) {
+ // set the client callback implementation
+ setCallback(callObject.mCallControlCallbackImpl)
+
+ // inject client control interface into the VoIP call object
+ callObject.setCallId(getCallId().toString())
+ callObject.setCallControl(this)
+
+ // Collect updates
+ currentCallEndpoint
+ .onEach { callObject.onCallEndpointChanged(it) }
+ .launchIn(coroutineScope)
+
+ availableEndpoints
+ .onEach { callObject.onAvailableCallEndpointsChanged(it) }
+ .launchIn(coroutineScope)
+
+ isMuted
+ .onEach { callObject.onMuteStateChanged(it) }
+ .launchIn(coroutineScope)
+ }
+ addCallRow(callObject)
+ } catch (e: CancellationException) {
+ Log.i(TAG, "addCallWithAttributes: cancellationException:$e")
+ }
+ }
+ }
+
+ private fun addCallRow(callObject: VoipCall) {
+ mCallObjects.add(CallRow(++mCallCount, callObject))
+ callObject.setCallAdapter(mAdapter)
+ updateCallList()
+ }
+
+ private fun updateCallList() {
+ runOnUiThread {
+ mAdapter?.notifyDataSetChanged()
+ }
+ }
+}
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/Utilities.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/Utilities.kt
new file mode 100644
index 0000000..37ff02f
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/Utilities.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.core.telecom.test
+
+import android.net.Uri
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallAttributesCompat
+import androidx.core.telecom.CallAttributesCompat.Companion.DIRECTION_INCOMING
+import androidx.core.telecom.CallAttributesCompat.Companion.DIRECTION_OUTGOING
+import androidx.core.telecom.CallAttributesCompat.Companion.CALL_TYPE_VIDEO_CALL
+
+@RequiresApi(34)
+class Utilities {
+ companion object {
+ const val APP_SCHEME = "MyCustomScheme"
+ const val ALL_CALL_CAPABILITIES = (CallAttributesCompat.SUPPORTS_SET_INACTIVE
+ or CallAttributesCompat.SUPPORTS_STREAM or CallAttributesCompat.SUPPORTS_TRANSFER)
+
+ // outgoing attributes constants
+ const val OUTGOING_NAME = "Darth Maul"
+ val OUTGOING_URI: Uri = Uri.parse("tel:6506958985")
+ // Define the minimal set of properties to start an outgoing call
+ var OUTGOING_CALL_ATTRIBUTES = CallAttributesCompat(
+ OUTGOING_NAME,
+ OUTGOING_URI,
+ DIRECTION_OUTGOING)
+
+ // incoming attributes constants
+ const val INCOMING_NAME = "Sundar Pichai"
+ val INCOMING_URI: Uri = Uri.parse("tel:6506958985")
+ // Define all possible properties for CallAttributes
+ val INCOMING_CALL_ATTRIBUTES =
+ CallAttributesCompat(
+ INCOMING_NAME,
+ INCOMING_URI,
+ DIRECTION_INCOMING,
+ CALL_TYPE_VIDEO_CALL,
+ ALL_CALL_CAPABILITIES)
+ }
+}
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/VoipCall.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/VoipCall.kt
new file mode 100644
index 0000000..d2decbb
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/VoipCall.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.core.telecom.test
+
+import android.telecom.DisconnectCause
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallControlCallback
+import androidx.core.telecom.CallControlScope
+import androidx.core.telecom.CallEndpointCompat
+
+@RequiresApi(34)
+class VoipCall {
+ private val TAG = VoipCall::class.simpleName
+
+ var mAdapter: CallListAdapter? = null
+ var mCallControl: CallControlScope? = null
+ var mCurrentEndpoint: CallEndpointCompat? = null
+ var mAvailableEndpoints: List<CallEndpointCompat>? = ArrayList()
+ var mIsMuted = false
+ var mTelecomCallId: String = ""
+
+ val mCallControlCallbackImpl = object : CallControlCallback {
+ override suspend fun onSetActive(): Boolean {
+ mAdapter?.updateCallState(mTelecomCallId, "Active")
+ return true
+ }
+ override suspend fun onSetInactive(): Boolean {
+ mAdapter?.updateCallState(mTelecomCallId, "Inactive")
+ return true
+ }
+ override suspend fun onAnswer(callType: Int): Boolean {
+ mAdapter?.updateCallState(mTelecomCallId, "Answered")
+ return true
+ }
+ override suspend fun onDisconnect(disconnectCause: DisconnectCause): Boolean {
+ mAdapter?.updateCallState(mTelecomCallId, "Disconnected")
+ return true
+ }
+ }
+
+ fun setCallControl(callControl: CallControlScope) {
+ mCallControl = callControl
+ }
+
+ fun setCallAdapter(adapter: CallListAdapter?) {
+ mAdapter = adapter
+ }
+
+ fun setCallId(callId: String) {
+ mTelecomCallId = callId
+ }
+
+ fun onCallEndpointChanged(endpoint: CallEndpointCompat) {
+ Log.i(TAG, "onCallEndpointChanged: endpoint=$endpoint")
+ mCurrentEndpoint = endpoint
+ mAdapter?.updateEndpoint(mTelecomCallId, endpoint.name.toString())
+ }
+
+ fun onAvailableCallEndpointsChanged(endpoints: List<CallEndpointCompat>) {
+ Log.i(TAG, "onAvailableCallEndpointsChanged:")
+ for (endpoint in endpoints) {
+ Log.i(TAG, "onAvailableCallEndpointsChanged: --> endpoint=$endpoint")
+ }
+ mAvailableEndpoints = endpoints
+ }
+
+ fun onMuteStateChanged(isMuted: Boolean) {
+ Log.i(TAG, "onMuteStateChanged: isMuted=$isMuted")
+ mIsMuted = isMuted
+ }
+
+ fun getEndpointType(type: Int): CallEndpointCompat? {
+ for (endpoint in mAvailableEndpoints!!) {
+ if (endpoint.type == type) {
+ return endpoint
+ }
+ }
+ return null
+ }
+}
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testapp/src/main/res/drawable/android.xml b/core/core-telecom/integration-tests/testapp/src/main/res/drawable/android.xml
new file mode 100644
index 0000000..dfa932e
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/res/drawable/android.xml
@@ -0,0 +1,35 @@
+<?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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="160dp"
+ android:height="160dp"
+ android:viewportHeight="432"
+ android:viewportWidth="432">
+
+ <!-- Safe zone = 66dp => 432 * (66 / 108) = 432 * 0.61 -->
+ <group
+ android:translateX="84"
+ android:translateY="84"
+ android:scaleX="0.61"
+ android:scaleY="0.61">
+
+ <path
+ android:fillColor="#3ddc84"
+ android:pathData="m322.02,167.89c12.141,-21.437 25.117,-42.497 36.765,-64.158 2.2993,-7.7566 -9.5332,-12.802 -13.555,-5.7796 -12.206,21.045 -24.375,42.112 -36.567,63.166 -57.901,-26.337 -127.00,-26.337 -184.90,0.0 -12.685,-21.446 -24.606,-43.441 -37.743,-64.562 -5.6074,-5.8390 -15.861,1.9202 -11.747,8.8889 12.030,20.823 24.092,41.629 36.134,62.446C47.866,200.90 5.0987,267.15 0.0,337.5c144.00,0.0 288.00,0.0 432.0,0.0C426.74,267.06 384.46,201.32 322.02,167.89ZM116.66,276.03c-13.076,0.58968 -22.531,-15.277 -15.773,-26.469 5.7191,-11.755 24.196,-12.482 30.824,-1.2128 7.8705,11.451 -1.1102,28.027 -15.051,27.682zM315.55,276.03c-13.076,0.58968 -22.531,-15.277 -15.773,-26.469 5.7191,-11.755 24.196,-12.482 30.824,-1.2128 7.8705,11.451 -1.1097,28.027 -15.051,27.682z"
+ android:strokeWidth="2" />
+ </group>
+</vector>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testapp/src/main/res/drawable/ic_launcher.xml b/core/core-telecom/integration-tests/testapp/src/main/res/drawable/ic_launcher.xml
new file mode 100644
index 0000000..481bbd7
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/res/drawable/ic_launcher.xml
@@ -0,0 +1,19 @@
+<?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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/android" />
+</layer-list>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testapp/src/main/res/layout/activity_main.xml b/core/core-telecom/integration-tests/testapp/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..ae035a5
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -0,0 +1,106 @@
+<?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.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
+ tools:context=".CallingMainActivity">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/app_name"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+
+ <CheckBox
+ android:id="@+id/VideoCallingCheckBox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="CAPABILITY_SUPPORTS_VIDEO_CALLING" />
+
+ <CheckBox
+ android:id="@+id/streamingCheckBox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="CAPABILITY_SUPPORTS_CALL_STREAMING" />
+
+ <Button
+ android:id="@+id/registerButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/register_button_text"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/add_out_call_button_text"
+ android:id="@+id/addOutgoingCall"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/add_in_call_button_text"
+ android:id="@+id/addIncomingCall"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+ </LinearLayout>
+
+ </LinearLayout>
+
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/callListRecyclerView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:itemCount="3" />
+
+ </LinearLayout>
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testapp/src/main/res/layout/call_row.xml b/core/core-telecom/integration-tests/testapp/src/main/res/layout/call_row.xml
new file mode 100644
index 0000000..9001096
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/res/layout/call_row.xml
@@ -0,0 +1,119 @@
+<?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:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/callNumber"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="call # -" />
+
+ <TextView
+ android:id="@+id/callIdTextView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="callId" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/callStateTextView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="currentCallState=[null]; " />
+
+ <TextView
+ android:id="@+id/endpointStateTextView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="currentEndpoint=[null]" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/activeButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Active" />
+
+ <Button
+ android:id="@+id/holdButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Hold" />
+
+ <Button
+ android:id="@+id/disconnectButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Disc." />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/earpieceButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="earpiece" />
+
+ <Button
+ android:id="@+id/speakerButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="speaker" />
+
+ <Button
+ android:id="@+id/bluetoothButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="bluetooth" />
+ </LinearLayout>
+
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testapp/src/main/res/values-land/dimens.xml b/core/core-telecom/integration-tests/testapp/src/main/res/values-land/dimens.xml
new file mode 100644
index 0000000..6a160a9
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/res/values-land/dimens.xml
@@ -0,0 +1,19 @@
+<!--
+ 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>
+ <dimen name="fab_margin">48dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testapp/src/main/res/values-w1240dp/dimens.xml b/core/core-telecom/integration-tests/testapp/src/main/res/values-w1240dp/dimens.xml
new file mode 100644
index 0000000..ba6cad4
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/res/values-w1240dp/dimens.xml
@@ -0,0 +1,19 @@
+<!--
+ 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>
+ <dimen name="fab_margin">200dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testapp/src/main/res/values-w600dp/dimens.xml b/core/core-telecom/integration-tests/testapp/src/main/res/values-w600dp/dimens.xml
new file mode 100644
index 0000000..6a160a9
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/res/values-w600dp/dimens.xml
@@ -0,0 +1,19 @@
+<!--
+ 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>
+ <dimen name="fab_margin">48dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testapp/src/main/res/values/colors.xml b/core/core-telecom/integration-tests/testapp/src/main/res/values/colors.xml
new file mode 100644
index 0000000..d70ea01
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/res/values/colors.xml
@@ -0,0 +1,21 @@
+<?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.
+ -->
+
+<resources>
+ <color name="black">#FF000000</color>
+ <color name="white">#FFFFFFFF</color>
+</resources>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testapp/src/main/res/values/dimens.xml b/core/core-telecom/integration-tests/testapp/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..fc04383
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/res/values/dimens.xml
@@ -0,0 +1,19 @@
+<!--
+ 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>
+ <dimen name="fab_margin">16dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testapp/src/main/res/values/strings.xml b/core/core-telecom/integration-tests/testapp/src/main/res/values/strings.xml
new file mode 100644
index 0000000..8d10c8c
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/res/values/strings.xml
@@ -0,0 +1,31 @@
+<!--
+ 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>
+ <string name="app_name">Telecom Jetpack Test App</string>
+ <string name="main_activity_name">Tel-Jetpack Activity</string>
+ <string name="register_button_text">Register App Phone Account</string>
+ <string name="add_out_call_button_text">+ Outgoing Call </string>
+ <string name="add_in_call_button_text">+ Incoming Call </string>
+
+ <string name="action_settings">Settings</string>
+ <!-- Strings used for fragments for navigation -->
+ <string name="first_fragment_label">First Fragment</string>
+ <string name="second_fragment_label">Second Fragment</string>
+ <string name="next">Next</string>
+ <string name="previous">Previous</string>
+
+</resources>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testapp/src/main/res/values/themes.xml b/core/core-telecom/integration-tests/testapp/src/main/res/values/themes.xml
new file mode 100644
index 0000000..14dbff3
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/res/values/themes.xml
@@ -0,0 +1,21 @@
+<!--
+ 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 xmlns:tools="http://schemas.android.com/tools">
+ <style name="Theme.Androidx.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
+ <style name="Theme.Androidx.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
+ <style name="AppTheme" parent="ThemeOverlay.AppCompat.Light" />
+</resources>
\ No newline at end of file
diff --git a/core/core-telecom/lint-baseline.xml b/core/core-telecom/lint-baseline.xml
new file mode 100644
index 0000000..0df9fc9
--- /dev/null
+++ b/core/core-telecom/lint-baseline.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.1.0-alpha07" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-beta03)" variant="all" version="8.1.0-alpha07">
+ <issue
+ id="ImplicitCastClassVerificationFailure"
+ message="This expression has type android.telecom.CallException (introduced in API level 34) but it used as type java.lang.Throwable (introduced in API level 1). Run-time class verification will not be able to validate this implicit cast on devices between these API levels."
+ errorLine1=" openResult.completeExceptionally(reason)"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/core/telecom/CallsManager.kt"/>
+ </issue>
+</issues>
diff --git a/core/core-telecom/src/androidTest/AndroidManifest.xml b/core/core-telecom/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..879f6d8
--- /dev/null
+++ b/core/core-telecom/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
+
+ <application>
+ <service
+ android:name="androidx.core.telecom.internal.JetpackConnectionService"
+ android:exported="true"
+ android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
+ <intent-filter>
+ <action android:name="android.telecom.ConnectionService"/>
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/CallEndpointCompatTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/CallEndpointCompatTest.kt
new file mode 100644
index 0000000..1711968
--- /dev/null
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/CallEndpointCompatTest.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.core.telecom
+
+import android.os.Build.VERSION_CODES
+import android.os.ParcelUuid
+import android.telecom.CallAudioState
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.internal.utils.EndpointUtils
+import androidx.test.filters.SdkSuppress
+import java.util.UUID
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@RequiresApi(VERSION_CODES.O)
+class CallEndpointCompatTest {
+
+ @Test
+ fun testCallEndpointConstructor() {
+ val name = "Endpoint"
+ val type = CallEndpointCompat.TYPE_EARPIECE
+ val identifier = ParcelUuid.fromString(UUID.randomUUID().toString())
+ val endpoint = CallEndpointCompat(name, type, identifier)
+ assertEquals(name, endpoint.name)
+ assertEquals(type, endpoint.type)
+ assertEquals(identifier, endpoint.identifier)
+ }
+
+ @SdkSuppress(minSdkVersion = VERSION_CODES.O)
+ @Test
+ fun testWrappingAudioStateIntoAEndpoint() {
+ val state = CallAudioState(false, CallAudioState.ROUTE_EARPIECE, 0)
+ val endpoint = EndpointUtils.toCallEndpointCompat(state)
+ assertEquals("EARPIECE", endpoint.name)
+ assertEquals(CallEndpointCompat.TYPE_EARPIECE, endpoint.type)
+ }
+
+ @SdkSuppress(minSdkVersion = VERSION_CODES.O)
+ @Test
+ fun testSupportedMask() {
+ val supportedRouteMask = CallAudioState.ROUTE_EARPIECE or
+ CallAudioState.ROUTE_SPEAKER or CallAudioState.ROUTE_WIRED_HEADSET
+ val state = CallAudioState(false, CallAudioState.ROUTE_EARPIECE, supportedRouteMask)
+ val endpoints = EndpointUtils.toCallEndpointsCompat(state)
+ assertEquals(3, endpoints.size)
+ }
+
+ @SdkSuppress(minSdkVersion = VERSION_CODES.O)
+ @Test
+ fun testCallAudioRouteToEndpointTypeMapping() {
+ assertEquals(
+ CallEndpointCompat.TYPE_EARPIECE,
+ EndpointUtils.mapRouteToType(CallAudioState.ROUTE_EARPIECE)
+ )
+ assertEquals(
+ CallEndpointCompat.TYPE_SPEAKER,
+ EndpointUtils.mapRouteToType(CallAudioState.ROUTE_SPEAKER)
+ )
+ assertEquals(
+ CallEndpointCompat.TYPE_WIRED_HEADSET,
+ EndpointUtils.mapRouteToType(CallAudioState.ROUTE_WIRED_HEADSET)
+ )
+ assertEquals(
+ CallEndpointCompat.TYPE_BLUETOOTH,
+ EndpointUtils.mapRouteToType(CallAudioState.ROUTE_BLUETOOTH)
+ )
+ assertEquals(
+ CallEndpointCompat.TYPE_STREAMING,
+ EndpointUtils.mapRouteToType(CallAudioState.ROUTE_STREAMING)
+ )
+ assertEquals(CallEndpointCompat.TYPE_UNKNOWN, EndpointUtils.mapRouteToType(-1))
+ }
+
+ @SdkSuppress(minSdkVersion = VERSION_CODES.O)
+ @Test
+ fun testTypeToRouteMapping() {
+ assertEquals(
+ CallAudioState.ROUTE_EARPIECE,
+ EndpointUtils.mapTypeToRoute(CallEndpointCompat.TYPE_EARPIECE)
+ )
+ assertEquals(
+ CallAudioState.ROUTE_SPEAKER,
+ EndpointUtils.mapTypeToRoute(CallEndpointCompat.TYPE_SPEAKER)
+ )
+ assertEquals(
+ CallAudioState.ROUTE_BLUETOOTH,
+ EndpointUtils.mapTypeToRoute(CallEndpointCompat.TYPE_BLUETOOTH)
+ )
+ assertEquals(
+ CallAudioState.ROUTE_WIRED_HEADSET,
+ EndpointUtils.mapTypeToRoute(CallEndpointCompat.TYPE_WIRED_HEADSET)
+ )
+ assertEquals(
+ CallAudioState.ROUTE_STREAMING,
+ EndpointUtils.mapTypeToRoute(CallEndpointCompat.TYPE_STREAMING)
+ )
+ assertEquals(
+ CallAudioState.ROUTE_EARPIECE,
+ EndpointUtils.mapTypeToRoute(-1)
+ )
+ }
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/CallsManagerTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/CallsManagerTest.kt
new file mode 100644
index 0000000..0231a1a
--- /dev/null
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/CallsManagerTest.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.core.telecom
+
+import android.content.Context
+import android.os.Build.VERSION_CODES
+import android.telecom.PhoneAccount.CAPABILITY_SELF_MANAGED
+import android.telecom.PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.internal.utils.Utils
+import androidx.core.telecom.internal.utils.BuildVersionAdapter
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertThrows
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@RequiresApi(VERSION_CODES.O)
+@SdkSuppress(minSdkVersion = VERSION_CODES.O /* api=26 */)
+class CallsManagerTest {
+ private val mTestClassName = "androidx.core.telecom.test"
+ private val mContext: Context = ApplicationProvider.getApplicationContext()
+ private val mCallsManager = CallsManager(mContext)
+
+ private val mV2Build = object : BuildVersionAdapter {
+ override fun hasPlatformV2Apis(): Boolean {
+ return true
+ }
+
+ override fun hasInvalidBuildVersion(): Boolean {
+ return false
+ }
+ }
+
+ private val mBackwardsCompatBuild = object : BuildVersionAdapter {
+ override fun hasPlatformV2Apis(): Boolean {
+ return false
+ }
+
+ override fun hasInvalidBuildVersion(): Boolean {
+ return false
+ }
+ }
+
+ private val mInvalidBuild = object : BuildVersionAdapter {
+ override fun hasPlatformV2Apis(): Boolean {
+ return false
+ }
+
+ override fun hasInvalidBuildVersion(): Boolean {
+ return true
+ }
+ }
+
+ @SmallTest
+ @Test
+ fun testGetPhoneAccountWithUBuild() {
+ Utils.setUtils(mV2Build)
+ val account = mCallsManager.getPhoneAccountHandleForPackage()
+ assertEquals(mTestClassName, account.componentName.className)
+ }
+
+ @SmallTest
+ @Test
+ fun testGetPhoneAccountWithUBuildWithTminusBuild() {
+ Utils.setUtils(mBackwardsCompatBuild)
+ val account = mCallsManager.getPhoneAccountHandleForPackage()
+ assertEquals(CallsManager.CONNECTION_SERVICE_CLASS, account.componentName.className)
+ }
+
+ @SmallTest
+ @Test
+ fun testGetPhoneAccountWithInvalidBuild() {
+ Utils.setUtils(mInvalidBuild)
+ assertThrows(UnsupportedOperationException::class.java) {
+ mCallsManager.getPhoneAccountHandleForPackage()
+ }
+ }
+
+ @SmallTest
+ @Test
+ fun testRegisterPhoneAccount() {
+ Utils.resetUtils()
+
+ if (Utils.hasInvalidBuildVersion()) {
+ assertThrows(UnsupportedOperationException::class.java) {
+ mCallsManager.registerAppWithTelecom(CallsManager.CAPABILITY_BASELINE)
+ }
+ } else {
+
+ mCallsManager.registerAppWithTelecom(CallsManager.CAPABILITY_BASELINE)
+ val account = mCallsManager.getBuiltPhoneAccount()!!
+
+ if (Utils.hasPlatformV2Apis()) {
+ assertTrue(
+ Utils.hasCapability(
+ CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS,
+ account.capabilities
+ )
+ )
+ } else {
+ assertTrue(
+ account.capabilities and CAPABILITY_SELF_MANAGED ==
+ CAPABILITY_SELF_MANAGED
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/JetpackConnectionServiceTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/JetpackConnectionServiceTest.kt
new file mode 100644
index 0000000..ee9f215
--- /dev/null
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/JetpackConnectionServiceTest.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.core.telecom
+
+import android.content.Context
+import android.net.Uri
+import android.os.Build.VERSION_CODES
+import android.telecom.Connection
+import android.telecom.ConnectionRequest
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.internal.CallChannels
+import androidx.core.telecom.internal.JetpackConnectionService
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import androidx.testutils.TestExecutor
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.asCoroutineDispatcher
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@RequiresApi(VERSION_CODES.O)
+@SdkSuppress(minSdkVersion = VERSION_CODES.O /* api=26 */)
+class JetpackConnectionServiceTest {
+
+ private val mContext: Context = ApplicationProvider.getApplicationContext()
+ private val mCallsManager = CallsManager(mContext)
+ private val mConnectionService = mCallsManager.mConnectionService
+ private val mHandle = mCallsManager.getPhoneAccountHandleForPackage()
+ private val workerExecutor = TestExecutor()
+ private val workerContext: CoroutineContext = workerExecutor.asCoroutineDispatcher()
+ private val callChannels = CallChannels()
+ private val TEST_CALL_ATTRIB_NAME = "Elon Musk"
+ private val TEST_CALL_ATTRIB_NUMBER = Uri.parse("tel:6506959001")
+
+ @After
+ fun onDestroy() {
+ callChannels.closeAllChannels()
+ JetpackConnectionService.mPendingConnectionRequests.clear()
+ }
+
+ /**
+ * Ensure an outgoing Connection object has its properties set before sending it off to the
+ * platform. The properties should reflect everything that is set in CallAttributes.
+ */
+ @SmallTest
+ @Test
+ fun testConnectionServicePropertiesAreSet_outgoingCall() {
+ // create the CallAttributes
+ val attributes = createCallAttributes(CallAttributesCompat.DIRECTION_OUTGOING)
+ // simulate the connection being created
+ val connection = mConnectionService.createSelfManagedConnection(
+ createConnectionRequest(attributes),
+ CallAttributesCompat.DIRECTION_OUTGOING
+ )
+ // verify / assert connection properties
+ verifyConnectionPropertiesBasics(connection)
+ assertEquals(Connection.STATE_DIALING, connection!!.state)
+ }
+
+ /**
+ * Ensure an incoming Connection object has its properties set before sending it off to the
+ * platform. The properties should reflect everything that is set in CallAttributes.
+ */
+ @SmallTest
+ @Test
+ fun testConnectionServicePropertiesAreSet_incomingCall() {
+ // create the CallAttributes
+ val attributes = createCallAttributes(CallAttributesCompat.DIRECTION_INCOMING)
+ // simulate the connection being created
+ val connection = mConnectionService.createSelfManagedConnection(
+ createConnectionRequest(attributes),
+ CallAttributesCompat.DIRECTION_INCOMING
+ )
+ // verify / assert connection properties
+ verifyConnectionPropertiesBasics(connection)
+ assertEquals(Connection.STATE_RINGING, connection!!.state)
+ }
+
+ private fun verifyConnectionPropertiesBasics(connection: Connection?) {
+ // assert it's not null
+ assertNotNull(connection)
+ // unwrap for testing
+ val unwrappedConnection = connection!!
+ // assert all the properties are the same
+ assertEquals(TEST_CALL_ATTRIB_NAME, unwrappedConnection.callerDisplayName)
+ assertEquals(TEST_CALL_ATTRIB_NUMBER, unwrappedConnection.address)
+ assertEquals(
+ Connection.CAPABILITY_HOLD,
+ unwrappedConnection.connectionCapabilities
+ and Connection.CAPABILITY_HOLD
+ )
+ assertEquals(
+ Connection.CAPABILITY_SUPPORT_HOLD,
+ unwrappedConnection.connectionCapabilities
+ and Connection.CAPABILITY_SUPPORT_HOLD
+ )
+ assertEquals(0, JetpackConnectionService.mPendingConnectionRequests.size)
+ }
+
+ private fun createCallAttributes(
+ callDirection: Int,
+ callType: Int? = CallAttributesCompat.CALL_TYPE_AUDIO_CALL
+ ): CallAttributesCompat {
+
+ val attributes: CallAttributesCompat = if (callType != null) {
+ CallAttributesCompat(
+ TEST_CALL_ATTRIB_NAME,
+ TEST_CALL_ATTRIB_NUMBER,
+ callDirection, callType
+ )
+ } else {
+ CallAttributesCompat(
+ TEST_CALL_ATTRIB_NAME,
+ TEST_CALL_ATTRIB_NUMBER,
+ callDirection
+ )
+ }
+
+ attributes.mHandle = mCallsManager.getPhoneAccountHandleForPackage()
+
+ return attributes
+ }
+
+ private fun createConnectionRequest(callAttributesCompat: CallAttributesCompat):
+ ConnectionRequest {
+ // wrap in PendingRequest
+ val pr = JetpackConnectionService.PendingConnectionRequest(
+ callAttributesCompat, callChannels, workerContext, null
+ )
+ // add to the list of pendingRequests
+ JetpackConnectionService.mPendingConnectionRequests.add(pr)
+ // create a ConnectionRequest
+ return ConnectionRequest(mHandle, TEST_CALL_ATTRIB_NUMBER, null)
+ }
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/AndroidManifest.xml b/core/core-telecom/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8ea6753
--- /dev/null
+++ b/core/core-telecom/src/main/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+ <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
+
+ <application>
+ <service
+ android:name="androidx.core.telecom.internal.JetpackConnectionService"
+ android:exported="true"
+ android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
+ <intent-filter>
+ <action android:name="android.telecom.ConnectionService"/>
+ </intent-filter>
+ </service>
+
+ </application>
+
+</manifest>
diff --git a/core/core-telecom/src/main/java/androidx/core/androidx-core-core-telecom-documentation.md b/core/core-telecom/src/main/java/androidx/core/androidx-core-core-telecom-documentation.md
new file mode 100644
index 0000000..bfb5ecc
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/androidx-core-core-telecom-documentation.md
@@ -0,0 +1,7 @@
+# Module root
+
+<GROUPID> <ARTIFACTID>
+
+# Package androidx.core.telecom
+
+TODO: Document
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallAttributesCompat.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallAttributesCompat.kt
new file mode 100644
index 0000000..e3d3610
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallAttributesCompat.kt
@@ -0,0 +1,192 @@
+/*
+ * 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.core.telecom
+
+import android.net.Uri
+import android.telecom.PhoneAccountHandle
+import androidx.annotation.IntDef
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.core.telecom.internal.utils.CallAttributesUtils
+import androidx.core.telecom.internal.utils.Utils
+import java.util.Objects
+
+/**
+ * CallAttributes represents a set of properties that define a new Call. Applications should build
+ * an instance of this class and use [CallsManager.addCall] to start a new call with Telecom.
+ *
+ * @param displayName Display name of the person on the other end of the call
+ * @param address Address of the call. Note, this can be extended to a meeting link
+ * @param direction The direction (Outgoing/Incoming) of the new Call
+ * @param callType Information related to data being transmitted (voice, video, etc. )
+ * @param callCapabilities Allows a package to opt into capabilities on the telecom side,
+ * on a per-call basis
+ */
+class CallAttributesCompat constructor(
+ val displayName: CharSequence,
+ val address: Uri,
+ @Direction val direction: Int,
+ @CallType val callType: Int = CALL_TYPE_AUDIO_CALL,
+ @CallCapability val callCapabilities: Int = SUPPORTS_SET_INACTIVE
+) {
+ internal var mHandle: PhoneAccountHandle? = null
+
+ override fun toString(): String {
+ return "CallAttributes(" +
+ "displayName=[$displayName], " +
+ "address=[$address], " +
+ "direction=[${directionToString()}], " +
+ "callType=[${callTypeToString()}], " +
+ "capabilities=[${capabilitiesToString()}])"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is CallAttributesCompat &&
+ displayName == other.displayName &&
+ address == other.address &&
+ direction == other.direction &&
+ callType == other.callType &&
+ callCapabilities == other.callCapabilities
+ }
+
+ override fun hashCode(): Int {
+ return Objects.hash(displayName, address, direction, callType, callCapabilities)
+ }
+
+ companion object {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(DIRECTION_INCOMING, DIRECTION_OUTGOING)
+ @Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER)
+ annotation class Direction
+
+ /**
+ * Indicates that the call is an incoming call.
+ */
+ const val DIRECTION_INCOMING = 1
+
+ /**
+ * Indicates that the call is an outgoing call.
+ */
+ const val DIRECTION_OUTGOING = 2
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(CALL_TYPE_AUDIO_CALL, CALL_TYPE_VIDEO_CALL)
+ @Target(AnnotationTarget.TYPE, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.PROPERTY)
+ annotation class CallType
+
+ /**
+ * Used when answering or dialing a call to indicate that the call does not have a video
+ * component
+ */
+ const val CALL_TYPE_AUDIO_CALL = 1
+
+ /**
+ * Indicates video transmission is supported
+ */
+ const val CALL_TYPE_VIDEO_CALL = 2
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(SUPPORTS_SET_INACTIVE, SUPPORTS_STREAM, SUPPORTS_TRANSFER, flag = true)
+ @Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER)
+ annotation class CallCapability
+
+ /**
+ * This call being created can be set to inactive (traditionally referred to as hold). This
+ * means that once a new call goes active, if the active call needs to be held in order to
+ * place or receive an incoming call, the active call will be placed on hold. otherwise,
+ * the active call may be disconnected.
+ */
+ const val SUPPORTS_SET_INACTIVE = 1 shl 1
+
+ /**
+ * This call can be streamed from a root device to another device to continue the call
+ * without completely transferring it. The call continues to take place on the source
+ * device, however media and control are streamed to another device.
+ */
+ const val SUPPORTS_STREAM = 1 shl 2
+
+ /**
+ * This call can be completely transferred from one endpoint to another.
+ */
+ const val SUPPORTS_TRANSFER = 1 shl 3
+ }
+
+ @RequiresApi(34)
+ internal fun toCallAttributes(
+ phoneAccountHandle: PhoneAccountHandle
+ ): android.telecom.CallAttributes {
+ return CallAttributesUtils.Api34PlusImpl.toTelecomCallAttributes(
+ phoneAccountHandle,
+ direction,
+ displayName,
+ address,
+ callType,
+ callCapabilities
+ )
+ }
+
+ private fun directionToString(): String {
+ return if (direction == DIRECTION_OUTGOING) {
+ "Outgoing"
+ } else {
+ "Incoming"
+ }
+ }
+
+ private fun callTypeToString(): String {
+ return if (callType == CALL_TYPE_AUDIO_CALL) {
+ "Audio"
+ } else {
+ "Video"
+ }
+ }
+
+ internal fun hasSupportsSetInactiveCapability(): Boolean {
+ return Utils.hasCapability(SUPPORTS_SET_INACTIVE, callCapabilities)
+ }
+
+ private fun hasStreamCapability(): Boolean {
+ return Utils.hasCapability(SUPPORTS_STREAM, callCapabilities)
+ }
+
+ private fun hasTransferCapability(): Boolean {
+ return Utils.hasCapability(SUPPORTS_TRANSFER, callCapabilities)
+ }
+
+ private fun capabilitiesToString(): String {
+ val sb = StringBuilder()
+ sb.append("[")
+ if (hasSupportsSetInactiveCapability()) {
+ sb.append("SetInactive")
+ }
+ if (hasStreamCapability()) {
+ sb.append(", Stream")
+ }
+ if (hasTransferCapability()) {
+ sb.append(", Transfer")
+ }
+ sb.append("])")
+ return sb.toString()
+ }
+
+ internal fun isOutgoingCall(): Boolean {
+ return direction == DIRECTION_OUTGOING
+ }
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallControlCallback.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallControlCallback.kt
new file mode 100644
index 0000000..15bc1d5
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallControlCallback.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.core.telecom
+
+/**
+ * CallControlCallback relays call updates (that require a response) from the Telecom framework out
+ * to the application. This can include operations which the app must implement on a Call due to the
+ * presence of other calls on the device, requests relayed from a Bluetooth device, or from another
+ * calling surface.
+ *
+ * <p>
+ * All CallControlCallbacks are transactional, meaning that a client must
+ * complete the suspend fun with a [Boolean] response in order to complete the
+ * CallControlCallback. If the operation has been completed, the [suspend fun] should return
+ * true. Otherwise, the suspend fun should be returned with a false to represent the
+ * CallControlCallback cannot be completed on the client side.
+ *
+ * <p>
+ * Note: Each CallEventCallback has a timeout of 5000 milliseconds. Failing to complete the
+ * suspend fun before the timeout will result in a failed transaction.
+ */
+interface CallControlCallback {
+ /**
+ * Telecom is informing your VoIP application to set the call active. Telecom is requesting
+ * this on behalf of an system service (e.g. Automotive service) or a device (e.g. Wearable).
+ *
+ * @return true to indicate your VoIP application can set the call (that corresponds to this
+ * CallControlCallback) to active. Otherwise, return false to indicate your application is
+ * unable to process the request and telecom will cancel the external request.
+ */
+ suspend fun onSetActive(): Boolean
+
+ /**
+ * Telecom is informing your VoIP application to set the call inactive. This is the same as
+ * holding a call for two endpoints but can be extended to setting a meeting inactive. Telecom
+ * is requesting this on behalf of an system service (e.g. Automotive service) or a device (e.g.
+ * Wearable).
+ *
+ * Note: Your app must stop using the microphone and playing incoming media when returning.
+ *
+ * @return true to indicate your VoIP application can transition the call state to inactive.
+ * Otherwise, return false to indicate your application is unable to process the request and
+ * telecom will cancel the external request.
+ */
+ suspend fun onSetInactive(): Boolean
+
+ /**
+ * Telecom is informing your VoIP application to answer an incoming call and set it to active.
+ * Telecom is requesting this on behalf of an system service (e.g. Automotive service) or a
+ * device (e.g. Wearable).
+ *
+ * @param callType that call is requesting to be answered as.
+ *
+ * @return true to indicate your VoIP application can answer the call with the given
+ * [CallAttributesCompat.Companion.CallType]. Otherwise, return false to indicate your application is
+ * unable to process the request and telecom will cancel the external request.
+ */
+ suspend fun onAnswer(@CallAttributesCompat.Companion.CallType callType: Int): Boolean
+
+ /**
+ * Telecom is informing your VoIP application to disconnect the call. Telecom is requesting this
+ * on behalf of an system service (e.g. Automotive service) or a device (e.g. Wearable).
+ *
+ * @param disconnectCause represents the cause for disconnecting the call.
+ *
+ * @return true when your VoIP application has disconnected the call. Otherwise, return false to
+ * indicate your application is unable to process the request. However, telecom will still
+ * disconnect and untrack the call.
+ */
+ suspend fun onDisconnect(disconnectCause: android.telecom.DisconnectCause): Boolean
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallControlScope.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallControlScope.kt
new file mode 100644
index 0000000..e72eb6e
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallControlScope.kt
@@ -0,0 +1,144 @@
+/*
+ * 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.core.telecom
+
+import android.os.ParcelUuid
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * DSL interface to provide and receive updates about a single call session. The scope should be
+ * used to provide updates to the call state and receive updates about a call state. Example usage:
+ * <pre>
+ * // initiate a call and control via the CallControlScope
+ * mCallsManager.addCall(callAttributes) { // This block represents the CallControlScope
+ *
+ * // set your implementation of [CallControlCallback]
+ * setCallback(myCallControlCallbackImplementation)
+ *
+ * // UI flow sends an update to a call state, relay the update to Telecom
+ * disconnectCallButton.setOnClickListener {
+ * val wasSuccessful = disconnect(reason) // waits for telecom async. response
+ * // update UI
+ * }
+ *
+ * // Collect updates
+ * currentCallEndpoint
+ * .onEach { // access the new [CallEndpoint] here }
+ * .launchIn(coroutineScope)
+ * }
+ * <pre>
+ */
+interface CallControlScope {
+ /**
+ * This method should be the first method called within the [CallControlScope] and your VoIP
+ * application should pass in a valid implementation of [CallControlCallback].
+ *
+ * <p>
+ * Failing to call this API may result in your VoIP process being killed or an error to occur.
+ */
+ @Suppress("ExecutorRegistration")
+ fun setCallback(callControlCallback: CallControlCallback)
+
+ /**
+ * @return the 128-bit universally unique identifier Telecom assigned to this CallControlScope.
+ * This id can be helpful for debugging when dumping the telecom system.
+ */
+ fun getCallId(): ParcelUuid
+
+ /**
+ * Inform Telecom that your app wants to make this call active. This method should be called
+ * when either an outgoing call is ready to go active or a held call is ready to go active
+ * again. For incoming calls that are ready to be answered, use [answer].
+ *
+ * Telecom will return true if your app is able to set the call active. Otherwise false will
+ * be returned (ex. another call is active and telecom cannot set this call active until the
+ * other call is held or disconnected)
+ */
+ suspend fun setActive(): Boolean
+
+ /**
+ * Inform Telecom that your app wants to make this call inactive. This the same as hold for two
+ * call endpoints but can be extended to setting a meeting to inactive.
+ *
+ * Telecom will return true if your app is able to set the call inactive. Otherwise, false will
+ * be returned.
+ */
+ suspend fun setInactive(): Boolean
+
+ /**
+ * Inform Telecom that your app wants to make this incoming call active. For outgoing calls
+ * and calls that have been placed on hold, use [setActive].
+ *
+ * @param [callType] that call is to be answered as.
+ *
+ * Telecom will return true if your app is able to answer the call. Otherwise false will
+ * be returned (ex. another call is active and telecom cannot set this call active until the
+ * other call is held or disconnected) which means that your app cannot answer this call at
+ * this time.
+ */
+ suspend fun answer(@CallAttributesCompat.Companion.CallType callType: Int): Boolean
+
+ /**
+ * Inform Telecom that your app wishes to disconnect the call and remove the call from telecom
+ * tracking.
+ *
+ * @param disconnectCause represents the cause for disconnecting the call. The only valid
+ * codes for the [android.telecom.DisconnectCause] passed in are:
+ * <ul>
+ * <li>[DisconnectCause#LOCAL]</li>
+ * <li>[DisconnectCause#REMOTE]</li>
+ * <li>[DisconnectCause#REJECTED]</li>
+ * <li>[DisconnectCause#MISSED]</li>
+ * </ul>
+ *
+ * Telecom will always return true unless the call has already been disconnected.
+ *
+ * <p>
+ * Note: After the call has been successfully disconnected, calling any [CallControlScope] will
+ * result in a false to be returned.
+ */
+ suspend fun disconnect(disconnectCause: android.telecom.DisconnectCause): Boolean
+
+ /**
+ * Request a [CallEndpointCompat] change. Clients should not define their own [CallEndpointCompat] when
+ * requesting a change. Instead, the new [endpoint] should be one of the valid [CallEndpointCompat]s
+ * provided by [availableEndpoints].
+ *
+ * @param endpoint The [CallEndpointCompat] to change to.
+ *
+ * Telecom will return true if your app is able to switch to the requested new endpoint.
+ * Otherwise false will be returned.
+ */
+ suspend fun requestEndpointChange(endpoint: CallEndpointCompat): Boolean
+
+ /**
+ * Collect the new [CallEndpointCompat] through which call media flows (i.e. speaker,
+ * bluetooth, etc.).
+ */
+ val currentCallEndpoint: Flow<CallEndpointCompat>
+
+ /**
+ * Collect the set of available [CallEndpointCompat]s reported by Telecom.
+ */
+ val availableEndpoints: Flow<List<CallEndpointCompat>>
+
+ /**
+ * Collect the current mute state of the call. This Flow is updated every time the mute state
+ * changes.
+ */
+ val isMuted: Flow<Boolean>
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallEndpointCompat.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallEndpointCompat.kt
new file mode 100644
index 0000000..81aa731
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallEndpointCompat.kt
@@ -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.core.telecom
+
+import android.os.Build.VERSION_CODES
+import android.os.ParcelUuid
+import androidx.annotation.IntDef
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.core.telecom.internal.utils.EndpointUtils
+import java.util.Objects
+import java.util.UUID
+
+/**
+ * Constructor for a [CallEndpointCompat] object.
+ *
+ * @param name Human-readable name associated with the endpoint
+ * @param type The type of endpoint through which call media being routed
+ * Allowed values:
+ * [.TYPE_EARPIECE]
+ * [.TYPE_BLUETOOTH]
+ * [.TYPE_WIRED_HEADSET]
+ * [.TYPE_SPEAKER]
+ * [.TYPE_STREAMING]
+ * [.TYPE_UNKNOWN]
+ * @param identifier A unique identifier for this endpoint on the device
+ */
+@RequiresApi(VERSION_CODES.O)
+class CallEndpointCompat(val name: CharSequence, val type: Int, val identifier: ParcelUuid) {
+ internal var mMackAddress: String = "-1"
+
+ override fun toString(): String {
+ return "CallEndpoint(" +
+ "name=[$name]," +
+ "type=[${EndpointUtils.endpointTypeToString(type)}]," +
+ "identifier=[$identifier])"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is CallEndpointCompat &&
+ name == other.name &&
+ type == other.type &&
+ identifier == other.identifier
+ }
+
+ override fun hashCode(): Int {
+ return Objects.hash(name, type, identifier)
+ }
+
+ companion object {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(
+ TYPE_UNKNOWN,
+ TYPE_EARPIECE,
+ TYPE_BLUETOOTH,
+ TYPE_WIRED_HEADSET,
+ TYPE_SPEAKER,
+ TYPE_STREAMING
+ )
+ @Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER)
+ annotation class EndpointType
+
+ /** Indicates that the type of endpoint through which call media flows is unknown type. */
+ const val TYPE_UNKNOWN = -1
+
+ /** Indicates that the type of endpoint through which call media flows is an earpiece. */
+ const val TYPE_EARPIECE = 1
+
+ /** Indicates that the type of endpoint through which call media flows is a Bluetooth. */
+ const val TYPE_BLUETOOTH = 2
+
+ /** Indicates that the type of endpoint through which call media flows is a wired headset. */
+ const val TYPE_WIRED_HEADSET = 3
+
+ /** Indicates that the type of endpoint through which call media flows is a speakerphone. */
+ const val TYPE_SPEAKER = 4
+
+ /** Indicates that the type of endpoint through which call media flows is an external. */
+ const val TYPE_STREAMING = 5
+ }
+
+ internal constructor(name: String, @EndpointType type: Int) :
+ this(name, type, ParcelUuid(UUID.randomUUID())) {
+ }
+
+ internal constructor(name: String, @EndpointType type: Int, address: String) : this(
+ name,
+ type
+ ) {
+ mMackAddress = address
+ }
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallException.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallException.kt
new file mode 100644
index 0000000..8677df2
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallException.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.core.telecom
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+
+/**
+ * This class defines exceptions that can be thrown when using [androidx.core.telecom] APIs.
+ */
+class CallException(
+ @CallErrorCode val code: Int = ERROR_UNKNOWN_CODE,
+ message: String? = codeToMessage(code)
+) : RuntimeException(message) {
+
+ override fun toString(): String {
+ return "CallException( code=[$code], message=[$message])"
+ }
+
+ companion object {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(ERROR_UNKNOWN_CODE, ERROR_CALLBACKS_CODE)
+ annotation class CallErrorCode
+
+ /**
+ * The operation has failed due to an unknown or unspecified error.
+ */
+ const val ERROR_UNKNOWN_CODE = 1
+
+ /**
+ * This error code is thrown whenever a call is added via [CallsManager.addCall] and the
+ * [CallControlScope.setCallback]s is not the first API called in the session block or at
+ * all. In order to avoid this exception, ensure your [CallControlScope] is calling
+ * [CallControlScope.setCallback]s.
+ */
+ const val ERROR_CALLBACKS_CODE = 2
+
+ internal const val ERROR_CALLBACKS_MSG: String = "Error, when using the " +
+ "[CallControlScope], you must first set the " +
+ "[androidx.core.telecom.CallControlCallback]s via [CallControlScope]#[setCallback]"
+
+ internal const val ERROR_BUILD_VERSION: String = "Core-Telecom only supports builds from" +
+ " Oreo (Android 8) and above. In order to utilize Core-Telecom, your device must" +
+ " be updated."
+
+ internal fun codeToMessage(@CallErrorCode code: Int): String {
+ when (code) {
+ ERROR_CALLBACKS_CODE -> return ERROR_CALLBACKS_MSG
+ }
+ return "An Unknown Error has occurred while using the Core-Telecom APIs"
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
new file mode 100644
index 0000000..25e9287
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
@@ -0,0 +1,270 @@
+/*
+ * 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.core.telecom
+
+import android.content.ComponentName
+import android.content.Context
+import android.os.Build.VERSION_CODES
+import android.os.OutcomeReceiver
+import android.os.Process
+import android.telecom.CallControl
+import android.telecom.CallException
+import android.telecom.PhoneAccount
+import android.telecom.PhoneAccountHandle
+import android.telecom.TelecomManager
+import androidx.annotation.IntDef
+import androidx.annotation.RequiresApi
+import androidx.annotation.RequiresPermission
+import androidx.annotation.RestrictTo
+import androidx.core.telecom.CallAttributesCompat.Companion.CALL_TYPE_VIDEO_CALL
+import androidx.core.telecom.internal.CallChannels
+import androidx.core.telecom.internal.CallSession
+import androidx.core.telecom.internal.CallSessionLegacy
+import androidx.core.telecom.internal.JetpackConnectionService
+import androidx.core.telecom.internal.utils.Utils
+import java.util.concurrent.CancellationException
+import java.util.concurrent.Executor
+import kotlin.coroutines.coroutineContext
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.job
+
+/**
+ * CallsManager allows VoIP applications to add their calls to the Android system service Telecom.
+ * By doing this, other services are aware of your VoIP application calls which leads to a more
+ * stable environment. For example, a wearable may be able to answer an incoming call from your
+ * application if the call is added to the Telecom system. VoIP applications that manage calls and
+ * do not inform the Telecom system may experience issues with resources (ex. microphone access).
+ *
+ * Note that access to some telecom information is permission-protected. Your app cannot access the
+ * protected information or gain access to protected functionality unless it has the appropriate
+ * permissions declared in its manifest file. Where permissions apply, they are noted in the method
+ * descriptions.
+ */
+@RequiresApi(VERSION_CODES.O)
+class CallsManager constructor(context: Context) {
+ private val mContext: Context = context
+ private var mPhoneAccount: PhoneAccount? = null
+ private val mTelecomManager: TelecomManager =
+ mContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
+ internal val mConnectionService: JetpackConnectionService = JetpackConnectionService()
+
+ // A single declared constant for a direct [Executor], since the coroutines primitives we invoke
+ // from the associated callbacks will perform their own dispatch as needed.
+ private val mDirectExecutor = Executor { it.run() }
+
+ companion object {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
+ @IntDef(
+ CAPABILITY_BASELINE,
+ CAPABILITY_SUPPORTS_VIDEO_CALLING,
+ CAPABILITY_SUPPORTS_CALL_STREAMING,
+ flag = true
+ )
+ @Retention(AnnotationRetention.SOURCE)
+ annotation class Capability
+
+ /**
+ * If your VoIP application does not want support any of the capabilities below, then your
+ * application can register with [CAPABILITY_BASELINE].
+ *
+ * Note: Calls can still be added and to the Telecom system but if other services request to
+ * perform a capability that is not supported by your application, Telecom will notify the
+ * service of the inability to perform the action instead of hitting an error.
+ */
+ const val CAPABILITY_BASELINE = 1 shl 0
+
+ /**
+ * Flag indicating that your VoIP application supports video calling.
+ * This is not an indication that your application is currently able to make a video
+ * call, but rather that it has the ability to make video calls (but not necessarily at this
+ * time).
+ *
+ * Whether a call can make a video call is ultimately controlled by
+ * [androidx.core.telecom.CallAttributesCompat]s capability
+ * [androidx.core.telecom.CallAttributesCompat.CallType]#[CALL_TYPE_VIDEO_CALL],
+ * which indicates that particular call is currently capable of making a video call.
+ */
+ const val CAPABILITY_SUPPORTS_VIDEO_CALLING = 1 shl 1
+
+ /**
+ * Flag indicating that this VoIP application supports call streaming. Call streaming means
+ * a call can be streamed from a root device to another device to continue the call
+ * without completely transferring it. The call continues to take place on the source
+ * device, however media and control are streamed to another device.
+ * [androidx.core.telecom.CallAttributesCompat.CallType]#[CAPABILITY_SUPPORTS_CALL_STREAMING]
+ * must also be set on per call basis in the event an application wants to gate this
+ * capability on a stricter basis.
+ */
+ const val CAPABILITY_SUPPORTS_CALL_STREAMING = 1 shl 2
+
+ // identifiers that indicate the call was established with core-telecom
+ internal const val PACKAGE_HANDLE_ID: String = "Jetpack"
+ internal const val PACKAGE_LABEL: String = "Telecom-Jetpack"
+ internal const val CONNECTION_SERVICE_CLASS =
+ "androidx.core.telecom.internal.JetpackConnectionService"
+ // fail messages specific to addCall
+ internal const val CALL_CREATION_FAILURE_MSG =
+ "The call failed to be added."
+ }
+
+ /**
+ * VoIP applications should look at each [Capability] annotated above and call this API in
+ * order to start adding calls via [addCall].
+ *
+ * Note: Registering capabilities must be done before calling [addCall] or an exception will
+ * be thrown by [addCall].
+ *
+ * @Throws UnsupportedOperationException if the device is on an invalid build
+ */
+ @RequiresPermission(value = "android.permission.MANAGE_OWN_CALLS")
+ fun registerAppWithTelecom(@Capability capabilities: Int) {
+ // verify the build version supports this API and throw an exception if not
+ Utils.verifyBuildVersion()
+ // start to build the PhoneAccount that will be registered via the platform API
+ var platformCapabilities: Int = PhoneAccount.CAPABILITY_SELF_MANAGED
+ val phoneAccountBuilder = PhoneAccount.builder(
+ getPhoneAccountHandleForPackage(),
+ PACKAGE_LABEL
+ )
+ // append additional capabilities if the device is on a U build or above
+ if (Utils.hasPlatformV2Apis()) {
+ platformCapabilities = PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS or
+ Utils.remapJetpackCapabilitiesToPlatformCapabilities(capabilities)
+ }
+ // remap and set capabilities
+ phoneAccountBuilder.setCapabilities(platformCapabilities)
+ // build and register the PhoneAccount via the Platform API
+ mPhoneAccount = phoneAccountBuilder.build()
+ mTelecomManager.registerPhoneAccount(mPhoneAccount)
+ }
+
+ /**
+ * Adds a new call with the specified [CallAttributesCompat] to the telecom service. This method
+ * can be used to add both incoming and outgoing calls.
+ *
+ * @param callAttributes attributes of the new call (incoming or outgoing, address, etc. )
+ * @param block DSL interface block that will run when the call is ready
+ *
+ * @Throws UnsupportedOperationException if the device is on an invalid build
+ * @Throws CancellationException if the call failed to be added
+ * @Throws CallException if [CallControlScope.setCallback] is not called first within the block
+ */
+ @RequiresPermission(value = "android.permission.MANAGE_OWN_CALLS")
+ @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+ @Suppress("ClassVerificationFailure")
+ suspend fun addCall(
+ callAttributes: CallAttributesCompat,
+ block: CallControlScope.() -> Unit
+ ) {
+ // This API is not supported for device running anything below Android O (26)
+ Utils.verifyBuildVersion()
+ // Setup channels for the CallEventCallbacks that only provide info updates
+ val callChannels = CallChannels()
+ callAttributes.mHandle = getPhoneAccountHandleForPackage()
+
+ // create a call session based off the build version
+ @RequiresApi(34)
+ if (Utils.hasPlatformV2Apis()) {
+ // CompletableDeferred pauses the execution of this method until the CallControl is
+ // returned by the Platform.
+ val openResult = CompletableDeferred<CallSession>(parent = coroutineContext.job)
+ // CallSession is responsible for handling both CallControl responses from the Platform
+ // and propagates CallControlCallbacks that originate in the Platform out to the client.
+ val callSession = CallSession(coroutineContext)
+
+ /**
+ * The Platform [android.telecom.TelecomManager.addCall] requires a
+ * [OutcomeReceiver]#<[CallControl], [CallException]> that will receive the async
+ * response of whether the call can be added.
+ */
+ val callControlOutcomeReceiver =
+ object : OutcomeReceiver<CallControl, CallException> {
+ override fun onResult(control: CallControl) {
+ callSession.setCallControl(control)
+ openResult.complete(callSession)
+ }
+
+ override fun onError(reason: CallException) {
+ // close all channels
+ callChannels.closeAllChannels()
+ // fail if we were still waiting for a CallControl
+ openResult.cancel(CancellationException(CALL_CREATION_FAILURE_MSG))
+ }
+ }
+
+ // leverage the platform API
+ mTelecomManager.addCall(
+ callAttributes.toCallAttributes(getPhoneAccountHandleForPackage()),
+ mDirectExecutor,
+ callControlOutcomeReceiver,
+ CallSession.CallControlCallbackImpl(callSession),
+ CallSession.CallEventCallbackImpl(callChannels)
+ )
+
+ openResult.await() /* wait for the platform to provide a CallControl object */
+ /* at this point in time we have CallControl object */
+ val scope =
+ CallSession.CallControlScopeImpl(openResult.getCompleted(), callChannels)
+
+ // Run the clients code with the session active and exposed via the CallControlScope
+ // interface implementation declared above.
+ scope.block()
+ } else {
+ // CompletableDeferred pauses the execution of this method until the Connection
+ // is created in JetpackConnectionService
+ val openResult =
+ CompletableDeferred<CallSessionLegacy>(parent = coroutineContext.job)
+
+ mConnectionService.createConnectionRequest(
+ mTelecomManager,
+ JetpackConnectionService.PendingConnectionRequest(
+ callAttributes, callChannels, coroutineContext, openResult
+ )
+ )
+
+ openResult.await()
+
+ val scope =
+ CallSessionLegacy.CallControlScopeImpl(openResult.getCompleted(), callChannels)
+
+ // Run the clients code with the session active and exposed via the
+ // CallControlScope interface implementation declared above.
+ scope.block()
+ }
+ }
+
+ internal fun getPhoneAccountHandleForPackage(): PhoneAccountHandle {
+ // This API is not supported for device running anything below Android O (26)
+ Utils.verifyBuildVersion()
+
+ val className = if (Utils.hasPlatformV2Apis()) {
+ mContext.packageName
+ } else {
+ CONNECTION_SERVICE_CLASS
+ }
+ return PhoneAccountHandle(
+ ComponentName(mContext.packageName, className),
+ PACKAGE_HANDLE_ID,
+ Process.myUserHandle()
+ )
+ }
+
+ internal fun getBuiltPhoneAccount(): PhoneAccount? {
+ return mPhoneAccount
+ }
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallChannels.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallChannels.kt
new file mode 100644
index 0000000..8989ca4
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallChannels.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.core.telecom.internal
+
+import androidx.core.telecom.CallEndpointCompat
+import kotlinx.coroutines.channels.Channel
+
+internal class CallChannels(
+ val currentEndpointChannel: Channel<CallEndpointCompat> = Channel(Channel.UNLIMITED),
+ val availableEndpointChannel: Channel<List<CallEndpointCompat>> = Channel(Channel.UNLIMITED),
+ val isMutedChannel: Channel<Boolean> = Channel(Channel.UNLIMITED)
+) {
+ fun closeAllChannels() {
+ currentEndpointChannel.close()
+ availableEndpointChannel.close()
+ isMutedChannel.close()
+ }
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSession.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSession.kt
new file mode 100644
index 0000000..a0e2cae
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSession.kt
@@ -0,0 +1,282 @@
+/*
+ * 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.core.telecom.internal
+
+import android.os.Bundle
+import android.os.OutcomeReceiver
+import android.os.ParcelUuid
+import android.telecom.CallException
+import android.telecom.DisconnectCause
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallControlCallback
+import androidx.core.telecom.CallControlScope
+import androidx.core.telecom.CallEndpointCompat
+import androidx.core.telecom.internal.utils.EndpointUtils
+import java.util.function.Consumer
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+
+@RequiresApi(34)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+@Suppress("ClassVerificationFailure")
+internal class CallSession(coroutineContext: CoroutineContext) {
+ private val mCoroutineContext = coroutineContext
+ private var mPlatformInterface: android.telecom.CallControl? = null
+ private var mClientInterface: CallControlCallback? = null
+
+ class CallControlCallbackImpl(private val callSession: CallSession) :
+ android.telecom.CallControlCallback {
+ override fun onSetActive(wasCompleted: Consumer<Boolean>) {
+ callSession.onSetActive(wasCompleted)
+ }
+
+ override fun onSetInactive(wasCompleted: Consumer<Boolean>) {
+ callSession.onSetInactive(wasCompleted)
+ }
+
+ override fun onAnswer(videoState: Int, wasCompleted: Consumer<Boolean>) {
+ callSession.onAnswer(videoState, wasCompleted)
+ }
+
+ override fun onDisconnect(
+ disconnectCause: DisconnectCause,
+ wasCompleted: Consumer<Boolean>
+ ) {
+ callSession.onDisconnect(disconnectCause, wasCompleted)
+ }
+
+ override fun onCallStreamingStarted(wasCompleted: Consumer<Boolean>) {
+ TODO("Implement with the CallStreaming code")
+ }
+ }
+
+ class CallEventCallbackImpl(private val callChannels: CallChannels) :
+ android.telecom.CallEventCallback {
+ override fun onCallEndpointChanged(
+ endpoint: android.telecom.CallEndpoint
+ ) {
+ callChannels.currentEndpointChannel.trySend(
+ EndpointUtils.Api34PlusImpl.toCallEndpointCompat(endpoint)
+ ).getOrThrow()
+ }
+
+ override fun onAvailableCallEndpointsChanged(
+ endpoints: List<android.telecom.CallEndpoint>
+ ) {
+ callChannels.availableEndpointChannel.trySend(
+ EndpointUtils.Api34PlusImpl.toCallEndpointsCompat(endpoints)
+ ).getOrThrow()
+ }
+
+ override fun onMuteStateChanged(isMuted: Boolean) {
+ callChannels.isMutedChannel.trySend(isMuted).getOrThrow()
+ }
+
+ override fun onCallStreamingFailed(reason: Int) {
+ TODO("Implement with the CallStreaming code")
+ }
+
+ override fun onEvent(event: String, extras: Bundle) {
+ TODO("Implement when events are agreed upon by ICS and package")
+ }
+ }
+
+ /**
+ * CallControl is set by CallsManager#addCall when the CallControl object is returned by the
+ * platform
+ */
+ fun setCallControl(control: android.telecom.CallControl) {
+ mPlatformInterface = control
+ }
+
+ /**
+ * pass in the clients callback implementation for CallControlCallback that is set in the
+ * CallsManager#addCall scope.
+ */
+ fun setCallControlCallback(clientCallbackImpl: CallControlCallback) {
+ mClientInterface = clientCallbackImpl
+ }
+
+ fun hasClientSetCallbacks(): Boolean {
+ return mClientInterface != null
+ }
+
+ /**
+ * Custom OutcomeReceiver that handles the Platform responses to a CallControl API call
+ */
+ inner class CallControlReceiver(deferred: CompletableDeferred<Boolean>) :
+ OutcomeReceiver<Void, CallException> {
+ private val mResultDeferred: CompletableDeferred<Boolean> = deferred
+
+ override fun onResult(r: Void?) {
+ mResultDeferred.complete(true)
+ }
+
+ override fun onError(error: CallException) {
+ mResultDeferred.complete(false)
+ }
+ }
+
+ fun getCallId(): ParcelUuid {
+ return mPlatformInterface!!.callId
+ }
+
+ suspend fun setActive(): Boolean {
+ val result: CompletableDeferred<Boolean> = CompletableDeferred()
+ mPlatformInterface?.setActive(Runnable::run, CallControlReceiver(result))
+ result.await()
+ return result.getCompleted()
+ }
+
+ suspend fun setInactive(): Boolean {
+ val result: CompletableDeferred<Boolean> = CompletableDeferred()
+ mPlatformInterface?.setInactive(Runnable::run, CallControlReceiver(result))
+ result.await()
+ return result.getCompleted()
+ }
+
+ suspend fun answer(videoState: Int): Boolean {
+ val result: CompletableDeferred<Boolean> = CompletableDeferred()
+ mPlatformInterface?.answer(videoState, Runnable::run, CallControlReceiver(result))
+ result.await()
+ return result.getCompleted()
+ }
+
+ suspend fun requestEndpointChange(endpoint: android.telecom.CallEndpoint): Boolean {
+ val result: CompletableDeferred<Boolean> = CompletableDeferred()
+ mPlatformInterface?.requestCallEndpointChange(
+ endpoint,
+ Runnable::run, CallControlReceiver(result)
+ )
+ result.await()
+ return result.getCompleted()
+ }
+
+ suspend fun disconnect(disconnectCause: DisconnectCause): Boolean {
+ val result: CompletableDeferred<Boolean> = CompletableDeferred()
+ mPlatformInterface?.disconnect(
+ disconnectCause,
+ Runnable::run,
+ CallControlReceiver(result)
+ )
+ result.await()
+ return result.getCompleted()
+ }
+
+ /**
+ * CallControlCallback
+ */
+ fun onSetActive(wasCompleted: Consumer<Boolean>) {
+ CoroutineScope(mCoroutineContext).launch {
+ val clientResponse: Boolean = mClientInterface!!.onSetActive()
+ wasCompleted.accept(clientResponse)
+ }
+ }
+
+ fun onSetInactive(wasCompleted: Consumer<Boolean>) {
+ CoroutineScope(mCoroutineContext).launch {
+ val clientResponse: Boolean = mClientInterface!!.onSetInactive()
+ wasCompleted.accept(clientResponse)
+ }
+ }
+
+ fun onAnswer(videoState: Int, wasCompleted: Consumer<Boolean>) {
+ CoroutineScope(mCoroutineContext).launch {
+ val clientResponse: Boolean = mClientInterface!!.onAnswer(videoState)
+ wasCompleted.accept(clientResponse)
+ }
+ }
+
+ fun onDisconnect(cause: DisconnectCause, wasCompleted: Consumer<Boolean>) {
+ CoroutineScope(mCoroutineContext).launch {
+ val clientResponse: Boolean = mClientInterface!!.onDisconnect(cause)
+ wasCompleted.accept(clientResponse)
+ }
+ }
+
+ /**
+ * =========================================================================================
+ * Simple implementation of [CallControlScope] with a [CallSession] as the session.
+ * =========================================================================================
+ */
+ class CallControlScopeImpl(
+ private val session: CallSession,
+ callChannels: CallChannels
+ ) : CallControlScope {
+ // handle actionable/handshake events that originate in the platform
+ // and require a response from the client
+ override fun setCallback(callControlCallback: CallControlCallback) {
+ session.setCallControlCallback(callControlCallback)
+ }
+
+ // handle requests that originate from the client and propagate into platform
+ // return the platforms response which indicates success of the request.
+ override fun getCallId(): ParcelUuid {
+ verifySessionCallbacks()
+ return session.getCallId()
+ }
+
+ override suspend fun setActive(): Boolean {
+ verifySessionCallbacks()
+ return session.setActive()
+ }
+
+ override suspend fun setInactive(): Boolean {
+ verifySessionCallbacks()
+ return session.setInactive()
+ }
+
+ override suspend fun answer(callType: Int): Boolean {
+ verifySessionCallbacks()
+ return session.answer(callType)
+ }
+
+ override suspend fun disconnect(disconnectCause: DisconnectCause): Boolean {
+ verifySessionCallbacks()
+ return session.disconnect(disconnectCause)
+ }
+
+ override suspend fun requestEndpointChange(endpoint: CallEndpointCompat):
+ Boolean {
+ verifySessionCallbacks()
+ return session.requestEndpointChange(
+ EndpointUtils.Api34PlusImpl.toCallEndpoint(endpoint)
+ )
+ }
+
+ // Send these events out to the client to collect
+ override val currentCallEndpoint: Flow<CallEndpointCompat> =
+ callChannels.currentEndpointChannel.receiveAsFlow()
+
+ override val availableEndpoints: Flow<List<CallEndpointCompat>> =
+ callChannels.availableEndpointChannel.receiveAsFlow()
+
+ override val isMuted: Flow<Boolean> =
+ callChannels.isMutedChannel.receiveAsFlow()
+
+ private fun verifySessionCallbacks() {
+ if (!session.hasClientSetCallbacks()) {
+ throw androidx.core.telecom.CallException(
+ androidx.core.telecom.CallException.ERROR_CALLBACKS_CODE)
+ }
+ }
+ }
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSessionLegacy.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSessionLegacy.kt
new file mode 100644
index 0000000..3a058dd
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSessionLegacy.kt
@@ -0,0 +1,321 @@
+/*
+ * 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.core.telecom.internal
+
+import android.bluetooth.BluetoothDevice
+import android.os.Build
+import android.os.Build.VERSION_CODES
+import android.os.ParcelUuid
+import android.telecom.CallAudioState
+import android.telecom.DisconnectCause
+import android.util.Log
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallControlCallback
+import androidx.core.telecom.CallControlScope
+import androidx.core.telecom.CallEndpointCompat
+import androidx.core.telecom.CallException
+import androidx.core.telecom.internal.utils.EndpointUtils
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+
+@RequiresApi(VERSION_CODES.O)
+internal class CallSessionLegacy(
+ private val id: ParcelUuid,
+ private val callChannels: CallChannels,
+ private val coroutineContext: CoroutineContext
+) : android.telecom.Connection() {
+ // instance vars
+ private val TAG: String = CallSessionLegacy::class.java.simpleName
+ private var mClientInterface: CallControlCallback? = null
+ private var mCachedBluetoothDevices: ArrayList<BluetoothDevice> = ArrayList()
+
+ companion object {
+ // CallStates. All these states mirror the values in the platform.
+ const val STATE_INITIALIZING = 0
+ const val STATE_NEW = 1
+ const val STATE_RINGING = 2
+ const val STATE_DIALING = 3
+ const val STATE_ACTIVE = 4
+ const val STATE_HOLDING = 5
+ const val STATE_DISCONNECTED = 6
+ }
+
+ fun setCallControlCallback(callControlCallback: CallControlCallback) {
+ mClientInterface = callControlCallback
+ }
+
+ fun hasClientSetCallbacks(): Boolean {
+ return mClientInterface != null
+ }
+
+ /**
+ * =========================================================================================
+ * Call State Updates
+ * =========================================================================================
+ */
+ override fun onStateChanged(state: Int) {
+ Log.v(TAG, "onStateChanged: state=${platformCallStateToString(state)}")
+ }
+
+ private fun platformCallStateToString(state: Int): String {
+ return when (state) {
+ STATE_INITIALIZING -> "INITIALIZING"
+ STATE_NEW -> "NEW"
+ STATE_DIALING -> "DIALING"
+ STATE_RINGING -> "RINGING"
+ STATE_ACTIVE -> "ACTIVE"
+ STATE_HOLDING -> "HOLDING"
+ STATE_DISCONNECTED -> "DISCONNECTED"
+ else -> "UNKNOWN"
+ }
+ }
+
+ /**
+ * =========================================================================================
+ * Audio Updates
+ * =========================================================================================
+ */
+ override fun onCallAudioStateChanged(state: CallAudioState) {
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.P) {
+ Api28PlusImpl.refreshBluetoothDeviceCache(mCachedBluetoothDevices, state)
+ }
+ callChannels.currentEndpointChannel.trySend(
+ EndpointUtils.toCallEndpointCompat(state)
+ ).getOrThrow()
+
+ callChannels.availableEndpointChannel.trySend(
+ EndpointUtils.toCallEndpointsCompat(state)
+ ).getOrThrow()
+
+ callChannels.isMutedChannel.trySend(state.isMuted).getOrThrow()
+ }
+
+ /**
+ * =========================================================================================
+ * CallControl
+ * =========================================================================================
+ */
+
+ fun getCallId(): ParcelUuid {
+ return id
+ }
+
+ fun answer(videoState: Int): Boolean {
+ setVideoState(videoState)
+ setActive()
+ return true
+ }
+
+ fun setConnectionActive(): Boolean {
+ setActive()
+ return true
+ }
+
+ fun setConnectionInactive(): Boolean {
+ setOnHold()
+ return true
+ }
+
+ fun setConnectionDisconnect(cause: DisconnectCause): Boolean {
+ setDisconnected(cause)
+ destroy()
+ return true
+ }
+
+ @Suppress("deprecation")
+ fun requestEndpointChange(callEndpoint: CallEndpointCompat): Boolean {
+ return if (Build.VERSION.SDK_INT < VERSION_CODES.P) {
+ Api26PlusImpl.setAudio(callEndpoint, this)
+ true
+ } else {
+ Api28PlusImpl.setAudio(callEndpoint, this, mCachedBluetoothDevices)
+ }
+ }
+
+ @Suppress("deprecation")
+ @RequiresApi(VERSION_CODES.O)
+ private object Api26PlusImpl {
+ @JvmStatic
+ @DoNotInline
+ fun setAudio(callEndpoint: CallEndpointCompat, connection: CallSessionLegacy) {
+ connection.setAudioRoute(EndpointUtils.mapTypeToRoute(callEndpoint.type))
+ }
+ }
+
+ @Suppress("deprecation")
+ @RequiresApi(VERSION_CODES.P)
+ private object Api28PlusImpl {
+ @JvmStatic
+ @DoNotInline
+ fun setAudio(
+ callEndpoint: CallEndpointCompat,
+ connection: CallSessionLegacy,
+ btCache: ArrayList<BluetoothDevice>
+ ): Boolean {
+ if (callEndpoint.type == CallEndpointCompat.TYPE_BLUETOOTH) {
+ val btDevice = getBluetoothDeviceFromEndpoint(btCache, callEndpoint)
+ if (btDevice != null) {
+ connection.requestBluetoothAudio(btDevice)
+ return true
+ }
+ return false
+ } else {
+ connection.setAudioRoute(EndpointUtils.mapTypeToRoute(callEndpoint.type))
+ return true
+ }
+ }
+
+ @JvmStatic
+ @DoNotInline
+ fun refreshBluetoothDeviceCache(
+ btCacheList: ArrayList<BluetoothDevice>,
+ state: CallAudioState
+ ) {
+ btCacheList.clear()
+ btCacheList.addAll(state.supportedBluetoothDevices)
+ }
+
+ @JvmStatic
+ @DoNotInline
+ fun getBluetoothDeviceFromEndpoint(
+ btCacheList: ArrayList<BluetoothDevice>,
+ endpoint: CallEndpointCompat
+ ): BluetoothDevice? {
+ for (btDevice in btCacheList) {
+ if (bluetoothDeviceMatchesEndpoint(btDevice, endpoint)) {
+ return btDevice
+ }
+ }
+ return null
+ }
+
+ fun bluetoothDeviceMatchesEndpoint(btDevice: BluetoothDevice, endpoint: CallEndpointCompat):
+ Boolean {
+ return (btDevice.address?.equals(endpoint.mMackAddress) ?: false)
+ }
+ }
+
+ /**
+ * =========================================================================================
+ * CallControlCallbacks
+ * =========================================================================================
+ */
+ override fun onAnswer(videoState: Int) {
+ CoroutineScope(coroutineContext).launch {
+ val clientCanAnswer = mClientInterface!!.onSetActive()
+ if (clientCanAnswer) {
+ setActive()
+ setVideoState(videoState)
+ }
+ }
+ }
+
+ override fun onUnhold() {
+ CoroutineScope(coroutineContext).launch {
+ val clientCanUnhold = mClientInterface!!.onSetActive()
+ if (clientCanUnhold) {
+ setActive()
+ }
+ }
+ }
+
+ override fun onHold() {
+ CoroutineScope(coroutineContext).launch {
+ val clientCanHold = mClientInterface!!.onSetInactive()
+ if (clientCanHold) {
+ setOnHold()
+ }
+ }
+ }
+
+ override fun onDisconnect() {
+ CoroutineScope(coroutineContext).launch {
+ mClientInterface!!.onDisconnect(
+ DisconnectCause(DisconnectCause.LOCAL)
+ )
+ setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
+ }
+ }
+
+ /**
+ * =========================================================================================
+ * Simple implementation of [CallControlScope] with a [CallSessionLegacy] as the session.
+ * =========================================================================================
+ */
+ class CallControlScopeImpl(
+ private val session: CallSessionLegacy,
+ callChannels: CallChannels
+ ) : CallControlScope {
+ // handle actionable/handshake events that originate in the platform
+ // and require a response from the client
+ override fun setCallback(callControlCallback: CallControlCallback) {
+ session.setCallControlCallback(callControlCallback)
+ }
+
+ // handle requests that originate from the client and propagate into platform
+ // return the platforms response which indicates success of the request.
+ override fun getCallId(): ParcelUuid {
+ verifySessionCallbacks()
+ return session.getCallId()
+ }
+
+ override suspend fun setActive(): Boolean {
+ verifySessionCallbacks()
+ return session.setConnectionActive()
+ }
+
+ override suspend fun setInactive(): Boolean {
+ verifySessionCallbacks()
+ return session.setConnectionInactive()
+ }
+
+ override suspend fun answer(callType: Int): Boolean {
+ verifySessionCallbacks()
+ return session.answer(callType)
+ }
+
+ override suspend fun disconnect(disconnectCause: DisconnectCause): Boolean {
+ verifySessionCallbacks()
+ return session.setConnectionDisconnect(disconnectCause)
+ }
+
+ override suspend fun requestEndpointChange(endpoint: CallEndpointCompat): Boolean {
+ verifySessionCallbacks()
+ return session.requestEndpointChange(endpoint)
+ }
+
+ // Send these events out to the client to collect
+ override val currentCallEndpoint: Flow<CallEndpointCompat> =
+ callChannels.currentEndpointChannel.receiveAsFlow()
+
+ override val availableEndpoints: Flow<List<CallEndpointCompat>> =
+ callChannels.availableEndpointChannel.receiveAsFlow()
+
+ override val isMuted: Flow<Boolean> =
+ callChannels.isMutedChannel.receiveAsFlow()
+
+ private fun verifySessionCallbacks() {
+ if (!session.hasClientSetCallbacks()) {
+ throw CallException(CallException.ERROR_CALLBACKS_CODE)
+ }
+ }
+ }
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt
new file mode 100644
index 0000000..d219910
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt
@@ -0,0 +1,243 @@
+/*
+ * 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.core.telecom.internal
+
+import android.os.Build
+import android.os.ParcelUuid
+import android.telecom.Connection
+import android.telecom.ConnectionRequest
+import android.telecom.ConnectionService
+import android.telecom.PhoneAccountHandle
+import android.telecom.TelecomManager
+import android.telecom.VideoProfile
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.RequiresPermission
+import androidx.core.telecom.CallAttributesCompat
+import androidx.core.telecom.CallsManager
+import androidx.core.telecom.internal.utils.Utils
+import java.util.UUID
+import java.util.concurrent.CancellationException
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+@RequiresApi(api = Build.VERSION_CODES.O)
+internal class JetpackConnectionService : ConnectionService() {
+ val TAG: String = JetpackConnectionService::class.java.simpleName.toString()
+
+ /**
+ * Wrap all the objects that are associated with a new CallSession request into a class
+ */
+ data class PendingConnectionRequest(
+ val callAttributes: CallAttributesCompat,
+ val callChannel: CallChannels,
+ val coroutineContext: CoroutineContext,
+ val completableDeferred: CompletableDeferred<CallSessionLegacy>?
+ )
+
+ companion object {
+ const val CONNECTION_CREATION_TIMEOUT: Long = 5000 // time in milli-seconds
+ var mPendingConnectionRequests: ArrayList<PendingConnectionRequest> = ArrayList()
+ }
+
+ /**
+ * Request the Platform create a new Connection with the properties given by [CallAttributesCompat].
+ * This request will have a timeout of [CONNECTION_CREATION_TIMEOUT] and be removed when the
+ * result is completed.
+ */
+ @RequiresPermission(value = "android.permission.MANAGE_OWN_CALLS")
+ fun createConnectionRequest(
+ telecomManager: TelecomManager,
+ pendingConnectionRequest: PendingConnectionRequest,
+ ) {
+ // add request to list
+ mPendingConnectionRequests.add(pendingConnectionRequest)
+
+ val extras = Utils.getBundleWithPhoneAccountHandle(
+ pendingConnectionRequest.callAttributes,
+ pendingConnectionRequest.callAttributes.mHandle!!
+ )
+
+ // Call into the platform to start call
+ if (pendingConnectionRequest.callAttributes.isOutgoingCall()) {
+ telecomManager.placeCall(
+ pendingConnectionRequest.callAttributes.address,
+ extras
+ )
+ } else {
+ telecomManager.addNewIncomingCall(
+ pendingConnectionRequest.callAttributes.mHandle,
+ extras
+ )
+ }
+
+ // create a job that times out if the connection cannot be created in x amount of time
+ CoroutineScope(pendingConnectionRequest.coroutineContext).launch {
+ delay(CONNECTION_CREATION_TIMEOUT)
+ if (!pendingConnectionRequest.completableDeferred!!.isCompleted) {
+ Log.i(
+ TAG, "The request to create a connection timed out. Cancelling the" +
+ "request to add the call to Telecom."
+ )
+ mPendingConnectionRequests.remove(pendingConnectionRequest)
+ pendingConnectionRequest.completableDeferred.cancel(
+ CancellationException(CallsManager.CALL_CREATION_FAILURE_MSG)
+ )
+ }
+ }
+ }
+
+ /**
+ * Outgoing Connections
+ */
+ override fun onCreateOutgoingConnection(
+ connectionManagerAccount: PhoneAccountHandle,
+ request: ConnectionRequest
+ ): Connection? {
+ return createSelfManagedConnection(
+ request,
+ CallAttributesCompat.DIRECTION_OUTGOING
+ )
+ }
+
+ override fun onCreateOutgoingConnectionFailed(
+ connectionManagerPhoneAccount: PhoneAccountHandle,
+ request: ConnectionRequest
+ ) {
+ val pendingRequest: PendingConnectionRequest? =
+ findTargetPendingConnectionRequest(
+ request,
+ CallAttributesCompat.DIRECTION_OUTGOING
+ )
+ pendingRequest?.completableDeferred?.cancel()
+
+ mPendingConnectionRequests.remove(pendingRequest)
+ }
+
+ /**
+ * Incoming Connections
+ */
+ override fun onCreateIncomingConnection(
+ connectionManagerPhoneAccount: PhoneAccountHandle,
+ request: ConnectionRequest
+ ): Connection? {
+ return createSelfManagedConnection(
+ request,
+ CallAttributesCompat.DIRECTION_INCOMING
+ )
+ }
+
+ override fun onCreateIncomingConnectionFailed(
+ connectionManagerPhoneAccount: PhoneAccountHandle,
+ request: ConnectionRequest
+ ) {
+ val pendingRequest: PendingConnectionRequest? =
+ findTargetPendingConnectionRequest(
+ request,
+ CallAttributesCompat.DIRECTION_INCOMING
+ )
+ pendingRequest?.completableDeferred?.cancel()
+ mPendingConnectionRequests.remove(pendingRequest)
+ }
+
+ internal fun createSelfManagedConnection(request: ConnectionRequest, direction: Int):
+ Connection? {
+ val targetRequest: PendingConnectionRequest =
+ findTargetPendingConnectionRequest(request, direction) ?: return null
+
+ val jetpackConnection = CallSessionLegacy(
+ ParcelUuid.fromString(UUID.randomUUID().toString()),
+ targetRequest.callChannel,
+ targetRequest.coroutineContext
+ )
+
+ // set display name
+ jetpackConnection.setCallerDisplayName(
+ targetRequest.callAttributes.displayName.toString(),
+ TelecomManager.PRESENTATION_ALLOWED
+ )
+
+ // set address
+ jetpackConnection.setAddress(
+ targetRequest.callAttributes.address,
+ TelecomManager.PRESENTATION_ALLOWED
+ )
+
+ // set the call state for the given direction
+ if (direction == CallAttributesCompat.DIRECTION_OUTGOING) {
+ jetpackConnection.setDialing()
+ } else {
+ jetpackConnection.setRinging()
+ }
+
+ // set the callType
+ if (targetRequest.callAttributes.callType
+ == CallAttributesCompat.CALL_TYPE_VIDEO_CALL
+ ) {
+ jetpackConnection.setVideoState(VideoProfile.STATE_BIDIRECTIONAL)
+ } else {
+ jetpackConnection.setVideoState(VideoProfile.STATE_AUDIO_ONLY)
+ }
+
+ // set the call capabilities
+ if (targetRequest.callAttributes.hasSupportsSetInactiveCapability()) {
+ jetpackConnection.setConnectionCapabilities(
+ Connection.CAPABILITY_HOLD or Connection.CAPABILITY_SUPPORT_HOLD
+ )
+ }
+
+ targetRequest.completableDeferred?.complete(jetpackConnection)
+ mPendingConnectionRequests.remove(targetRequest)
+
+ return jetpackConnection
+ }
+
+ /**
+ * Helper methods
+ */
+ private fun findTargetPendingConnectionRequest(
+ request: ConnectionRequest,
+ direction: Int
+ ): PendingConnectionRequest? {
+ for (pendingConnectionRequest in mPendingConnectionRequests) {
+ if (isSameAddress(pendingConnectionRequest.callAttributes, request) &&
+ isSameDirection(pendingConnectionRequest.callAttributes, direction) &&
+ isSameHandle(pendingConnectionRequest.callAttributes.mHandle, request)
+ ) {
+ return pendingConnectionRequest
+ }
+ }
+ return null
+ }
+
+ private fun isSameDirection(callAttributes: CallAttributesCompat, direction: Int): Boolean {
+ return (callAttributes.direction == direction)
+ }
+
+ private fun isSameAddress(
+ callAttributes: CallAttributesCompat,
+ request: ConnectionRequest
+ ): Boolean {
+ return request.address?.equals(callAttributes.address) ?: false
+ }
+
+ private fun isSameHandle(handle: PhoneAccountHandle?, request: ConnectionRequest): Boolean {
+ return request.accountHandle?.equals(handle) ?: false
+ }
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/BuildVersionAdapter.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/BuildVersionAdapter.kt
new file mode 100644
index 0000000..9c40d14
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/BuildVersionAdapter.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.core.telecom.internal.utils
+
+internal interface BuildVersionAdapter {
+ fun hasPlatformV2Apis(): Boolean
+ fun hasInvalidBuildVersion(): Boolean
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/CallAttributesUtils.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/CallAttributesUtils.kt
new file mode 100644
index 0000000..5148edd
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/CallAttributesUtils.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.core.telecom.internal.utils
+
+import android.net.Uri
+import android.telecom.PhoneAccountHandle
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallAttributesCompat
+
+@RequiresApi(34)
+internal class CallAttributesUtils {
+ internal object Api34PlusImpl {
+ @JvmStatic
+ @DoNotInline
+ fun toTelecomCallAttributes(
+ phoneAccountHandle: PhoneAccountHandle,
+ direction: Int,
+ displayName: CharSequence,
+ address: Uri,
+ callType: Int,
+ callCapabilities: Int
+ ): android.telecom.CallAttributes {
+ return android.telecom.CallAttributes.Builder(
+ phoneAccountHandle,
+ direction,
+ displayName,
+ address
+ )
+ .setCallType(remapCallType(callType))
+ .setCallCapabilities(remapCapabilities(callCapabilities))
+ .build()
+ }
+
+ private fun remapCallType(callType: Int): Int {
+ return if (callType == CallAttributesCompat.CALL_TYPE_AUDIO_CALL) {
+ android.telecom.CallAttributes.AUDIO_CALL
+ } else {
+ android.telecom.CallAttributes.VIDEO_CALL
+ }
+ }
+
+ private fun remapCapabilities(callCapabilities: Int): Int {
+ var bitMap: Int = 0
+ if (hasSupportsSetInactiveCapability(callCapabilities)) {
+ bitMap = bitMap or android.telecom.CallAttributes.SUPPORTS_SET_INACTIVE
+ }
+ if (hasStreamCapability(callCapabilities)) {
+ bitMap = bitMap or android.telecom.CallAttributes.SUPPORTS_STREAM
+ }
+ if (hasTransferCapability(callCapabilities)) {
+ bitMap = bitMap or android.telecom.CallAttributes.SUPPORTS_TRANSFER
+ }
+ return bitMap
+ }
+
+ private fun hasSupportsSetInactiveCapability(callCapabilities: Int): Boolean {
+ return Utils.hasCapability(CallAttributesCompat.SUPPORTS_SET_INACTIVE, callCapabilities)
+ }
+
+ private fun hasStreamCapability(callCapabilities: Int): Boolean {
+ return Utils.hasCapability(CallAttributesCompat.SUPPORTS_STREAM, callCapabilities)
+ }
+
+ private fun hasTransferCapability(callCapabilities: Int): Boolean {
+ return Utils.hasCapability(CallAttributesCompat.SUPPORTS_TRANSFER, callCapabilities)
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/EndpointUtils.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/EndpointUtils.kt
new file mode 100644
index 0000000..7075dc5
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/EndpointUtils.kt
@@ -0,0 +1,217 @@
+/*
+ * 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.core.telecom.internal.utils
+
+import android.bluetooth.BluetoothDevice
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES.P
+import android.telecom.CallAudioState
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallEndpointCompat
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal class EndpointUtils {
+
+ companion object {
+ fun toCallEndpointCompat(state: CallAudioState): CallEndpointCompat {
+ val type: Int = mapRouteToType(state.route)
+ return if (type == CallEndpointCompat.TYPE_BLUETOOTH && SDK_INT >= P) {
+ BluetoothApi28PlusImpl.getCallEndpointFromAudioState(state)
+ } else {
+ CallEndpointCompat(endpointTypeToString(type), type)
+ }
+ }
+
+ fun toCallEndpointsCompat(state: CallAudioState): List<CallEndpointCompat> {
+ val endpoints: ArrayList<CallEndpointCompat> = ArrayList()
+ val bitMask = state.supportedRouteMask
+ if (hasEarpieceType(bitMask)) {
+ endpoints.add(
+ CallEndpointCompat(
+ endpointTypeToString(CallEndpointCompat.TYPE_EARPIECE),
+ CallEndpointCompat.TYPE_EARPIECE
+ )
+ )
+ }
+ if (hasBluetoothType(bitMask)) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ endpoints.addAll(BluetoothApi28PlusImpl.getBluetoothEndpoints(state))
+ } else {
+ endpoints.add(
+ CallEndpointCompat(
+ endpointTypeToString(CallEndpointCompat.TYPE_BLUETOOTH),
+ CallEndpointCompat.TYPE_BLUETOOTH
+ )
+ )
+ }
+ }
+ if (hasWiredHeadsetType(bitMask)) {
+ endpoints.add(
+ CallEndpointCompat(
+ endpointTypeToString(CallEndpointCompat.TYPE_WIRED_HEADSET),
+ CallEndpointCompat.TYPE_WIRED_HEADSET
+ )
+ )
+ }
+ if (hasSpeakerType(bitMask)) {
+ endpoints.add(
+ CallEndpointCompat(
+ endpointTypeToString(CallEndpointCompat.TYPE_SPEAKER),
+ CallEndpointCompat.TYPE_SPEAKER
+ )
+ )
+ }
+ if (hasStreamingType(bitMask)) {
+ endpoints.add(
+ CallEndpointCompat(
+ endpointTypeToString(CallEndpointCompat.TYPE_STREAMING),
+ CallEndpointCompat.TYPE_STREAMING
+ )
+ )
+ }
+ return endpoints
+ }
+
+ private fun hasEarpieceType(bitMap: Int): Boolean {
+ return (bitMap.and(CallAudioState.ROUTE_EARPIECE)) == CallAudioState.ROUTE_EARPIECE
+ }
+
+ fun hasBluetoothType(bitMap: Int): Boolean {
+ return (bitMap.and(CallAudioState.ROUTE_BLUETOOTH)) == CallAudioState.ROUTE_BLUETOOTH
+ }
+
+ fun hasWiredHeadsetType(bitMap: Int): Boolean {
+ return (bitMap.and(CallAudioState.ROUTE_WIRED_HEADSET)
+ ) == CallAudioState.ROUTE_WIRED_HEADSET
+ }
+
+ fun hasSpeakerType(bitMap: Int): Boolean {
+ return (bitMap.and(CallAudioState.ROUTE_SPEAKER)) == CallAudioState.ROUTE_SPEAKER
+ }
+
+ fun hasStreamingType(bitMap: Int): Boolean {
+ return (bitMap.and(CallAudioState.ROUTE_STREAMING)) == CallAudioState.ROUTE_STREAMING
+ }
+
+ fun mapRouteToType(route: Int): @CallEndpointCompat.Companion.EndpointType Int {
+ return when (route) {
+ CallAudioState.ROUTE_EARPIECE -> CallEndpointCompat.TYPE_EARPIECE
+ CallAudioState.ROUTE_BLUETOOTH -> CallEndpointCompat.TYPE_BLUETOOTH
+ CallAudioState.ROUTE_WIRED_HEADSET -> CallEndpointCompat.TYPE_WIRED_HEADSET
+ CallAudioState.ROUTE_SPEAKER -> CallEndpointCompat.TYPE_SPEAKER
+ CallAudioState.ROUTE_STREAMING -> CallEndpointCompat.TYPE_STREAMING
+ else -> CallEndpointCompat.TYPE_UNKNOWN
+ }
+ }
+
+ fun mapTypeToRoute(route: Int): Int {
+ return when (route) {
+ CallEndpointCompat.TYPE_EARPIECE -> CallAudioState.ROUTE_EARPIECE
+ CallEndpointCompat.TYPE_BLUETOOTH -> CallAudioState.ROUTE_BLUETOOTH
+ CallEndpointCompat.TYPE_WIRED_HEADSET -> CallAudioState.ROUTE_WIRED_HEADSET
+ CallEndpointCompat.TYPE_SPEAKER -> CallAudioState.ROUTE_SPEAKER
+ CallEndpointCompat.TYPE_STREAMING -> CallAudioState.ROUTE_STREAMING
+ else -> CallAudioState.ROUTE_EARPIECE
+ }
+ }
+
+ fun endpointTypeToString(endpointType: Int): String {
+ return when (endpointType) {
+ CallEndpointCompat.TYPE_EARPIECE -> "EARPIECE"
+ CallEndpointCompat.TYPE_BLUETOOTH -> "BLUETOOTH"
+ CallEndpointCompat.TYPE_WIRED_HEADSET -> "WIRED_HEADSET"
+ CallEndpointCompat.TYPE_SPEAKER -> "SPEAKER"
+ CallEndpointCompat.TYPE_STREAMING -> "EXTERNAL"
+ else -> "UNKNOWN ($endpointType)"
+ }
+ }
+ }
+
+ @RequiresApi(34)
+ object Api34PlusImpl {
+ @JvmStatic
+ @DoNotInline
+ fun toCallEndpointCompat(endpoint: android.telecom.CallEndpoint):
+ CallEndpointCompat {
+ return CallEndpointCompat(
+ endpoint.endpointName,
+ endpoint.endpointType,
+ endpoint.identifier
+ )
+ }
+
+ @JvmStatic
+ @DoNotInline
+ fun toCallEndpointsCompat(endpoints: List<android.telecom.CallEndpoint>):
+ List<CallEndpointCompat> {
+ val res = ArrayList<CallEndpointCompat>()
+ for (e in endpoints) {
+ res.add(CallEndpointCompat(e.endpointName, e.endpointType, e.identifier))
+ }
+ return res
+ }
+
+ @JvmStatic
+ @DoNotInline
+ fun toCallEndpoint(e: CallEndpointCompat): android.telecom.CallEndpoint {
+ return android.telecom.CallEndpoint(e.name, e.type, e.identifier)
+ }
+ }
+
+ @RequiresApi(28)
+ object BluetoothApi28PlusImpl {
+ @JvmStatic
+ @DoNotInline
+ fun getBluetoothEndpoints(state: CallAudioState):
+ ArrayList<CallEndpointCompat> {
+ val endpoints: ArrayList<CallEndpointCompat> = ArrayList()
+ val supportedBluetoothDevices = state.supportedBluetoothDevices
+ for (bluetoothDevice in supportedBluetoothDevices) {
+ endpoints.add(getCallEndpointFromBluetoothDevice(bluetoothDevice))
+ }
+ return endpoints
+ }
+
+ @JvmStatic
+ @DoNotInline
+ fun getCallEndpointFromBluetoothDevice(btDevice: BluetoothDevice?): CallEndpointCompat {
+ var endpointName: String = "Bluetooth Device"
+ var endpointIdentity: String = "Unknown Address"
+ if (btDevice != null) {
+ endpointIdentity = btDevice.address
+ try {
+ endpointName = btDevice.name
+ } catch (e: SecurityException) {
+ // pass through
+ }
+ }
+ return CallEndpointCompat(
+ endpointName,
+ CallEndpointCompat.TYPE_BLUETOOTH,
+ endpointIdentity
+ )
+ }
+
+ @JvmStatic
+ @DoNotInline
+ fun getCallEndpointFromAudioState(state: CallAudioState): CallEndpointCompat {
+ return getCallEndpointFromBluetoothDevice(state.activeBluetoothDevice)
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/Utils.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/Utils.kt
new file mode 100644
index 0000000..114a180
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/Utils.kt
@@ -0,0 +1,137 @@
+/*
+ * 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.core.telecom.internal.utils
+
+import android.os.Build.VERSION
+import android.os.Build.VERSION_CODES
+import android.os.Bundle
+import android.telecom.PhoneAccount
+import android.telecom.PhoneAccountHandle
+import android.telecom.TelecomManager
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallAttributesCompat
+import androidx.core.telecom.CallException
+import androidx.core.telecom.CallsManager
+
+internal class Utils {
+ companion object {
+ private val defaultBuildAdapter =
+ object : BuildVersionAdapter {
+ /**
+ * Helper method that determines if the device has a build that contains the Telecom V2
+ * VoIP APIs. These include [TelecomManager#addCall], android.telecom.CallControl,
+ * android.telecom.CallEventCallback but are not limited to only those classes.
+ */
+ override fun hasPlatformV2Apis(): Boolean {
+ return VERSION.SDK_INT >= 34 || VERSION.CODENAME == "UpsideDownCake"
+ }
+
+ override fun hasInvalidBuildVersion(): Boolean {
+ return VERSION.SDK_INT < VERSION_CODES.O
+ }
+ }
+ private var mBuildVersion: BuildVersionAdapter = defaultBuildAdapter
+
+ internal fun setUtils(utils: BuildVersionAdapter) {
+ mBuildVersion = utils
+ }
+
+ internal fun resetUtils() {
+ mBuildVersion = defaultBuildAdapter
+ }
+
+ fun hasPlatformV2Apis(): Boolean {
+ return mBuildVersion.hasPlatformV2Apis()
+ }
+
+ fun hasInvalidBuildVersion(): Boolean {
+ return mBuildVersion.hasInvalidBuildVersion()
+ }
+
+ fun verifyBuildVersion() {
+ if (mBuildVersion.hasInvalidBuildVersion()) {
+ throw UnsupportedOperationException(CallException.ERROR_BUILD_VERSION)
+ }
+ }
+
+ fun remapJetpackCapabilitiesToPlatformCapabilities(
+ @CallsManager.Companion.Capability clientBitmapSelection: Int
+ ): Int {
+ var remappedCapabilities = 0
+
+ if (hasJetpackVideoCallingCapability(clientBitmapSelection)) {
+ remappedCapabilities =
+ PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING or
+ remappedCapabilities
+ }
+
+ if (hasJetpackSteamingCapability(clientBitmapSelection)) {
+ remappedCapabilities =
+ PhoneAccount.CAPABILITY_SUPPORTS_CALL_STREAMING or
+ remappedCapabilities
+ }
+ return remappedCapabilities
+ }
+
+ fun hasCapability(targetCapability: Int, bitMap: Int): Boolean {
+ return (bitMap.and(targetCapability)) == targetCapability
+ }
+
+ private fun hasJetpackVideoCallingCapability(bitMap: Int): Boolean {
+ return hasCapability(CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING, bitMap)
+ }
+
+ private fun hasJetpackSteamingCapability(bitMap: Int): Boolean {
+ return hasCapability(CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING, bitMap)
+ }
+
+ fun getBundleWithPhoneAccountHandle(
+ callAttributes: CallAttributesCompat,
+ handle: PhoneAccountHandle
+ ): Bundle {
+ return if (VERSION.SDK_INT >= VERSION_CODES.M) {
+ Api23PlusImpl.createExtras(callAttributes, handle)
+ } else {
+ Bundle()
+ }
+ }
+
+ @RequiresApi(VERSION_CODES.M)
+ private object Api23PlusImpl {
+ @JvmStatic
+ @DoNotInline
+ fun createExtras(
+ callAttributes: CallAttributesCompat,
+ handle: PhoneAccountHandle
+ ): Bundle {
+ val extras = Bundle()
+ extras.putParcelable(
+ TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
+ handle
+ )
+ if (!callAttributes.isOutgoingCall()) {
+ extras.putParcelable(
+ TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
+ callAttributes.address
+ )
+ }
+ return extras
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/core-testing/build.gradle b/core/core-testing/build.gradle
index fdc6f76..1708420 100644
--- a/core/core-testing/build.gradle
+++ b/core/core-testing/build.gradle
@@ -41,9 +41,9 @@
}
androidx {
- name = "androidx.core:core-testing"
+ name = "Core Testing"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.CORE
inceptionYear = "2023"
- description = "Write tests using core APIs."
+ description = "Provides extensions for tests using Core APIs."
}
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index 4e95224..76925d1 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -156,6 +156,15 @@
field public static final int TOTAL_INDEX = 0; // 0x0
}
+ public final class GrammaticalInflectionManagerCompat {
+ method @AnyThread public static int getApplicationGrammaticalGender(android.content.Context);
+ method @AnyThread public static void setRequestedApplicationGrammaticalGender(android.content.Context, int);
+ field public static final int GRAMMATICAL_GENDER_FEMININE = 2; // 0x2
+ field public static final int GRAMMATICAL_GENDER_MASCULINE = 3; // 0x3
+ field public static final int GRAMMATICAL_GENDER_NEUTRAL = 1; // 0x1
+ field public static final int GRAMMATICAL_GENDER_NOT_SPECIFIED = 0; // 0x0
+ }
+
@Deprecated public abstract class JobIntentService extends android.app.Service {
ctor @Deprecated public JobIntentService();
method @Deprecated public static void enqueueWork(android.content.Context, Class<?>, int, android.content.Intent);
@@ -793,6 +802,7 @@
public final class NotificationManagerCompat {
method public boolean areNotificationsEnabled();
+ method public boolean canUseFullScreenIntent();
method public void cancel(int);
method public void cancel(String?, int);
method public void cancelAll();
@@ -953,6 +963,7 @@
}
public final class ServiceCompat {
+ method public static void startForeground(android.app.Service, int, android.app.Notification, int);
method public static void stopForeground(android.app.Service, int);
field public static final int START_STICKY = 1; // 0x1
field public static final int STOP_FOREGROUND_DETACH = 2; // 0x2
@@ -1104,7 +1115,6 @@
ctor protected FileProvider(@XmlRes int);
method public int delete(android.net.Uri, String?, String![]?);
method public String? getType(android.net.Uri);
- method public String? getTypeAnonymous(android.net.Uri);
method public static android.net.Uri! getUriForFile(android.content.Context, String, java.io.File);
method public static android.net.Uri getUriForFile(android.content.Context, String, java.io.File, String);
method public android.net.Uri! insert(android.net.Uri, android.content.ContentValues);
@@ -2046,6 +2056,26 @@
}
+package androidx.core.service.quicksettings {
+
+ public class PendingIntentActivityWrapper {
+ ctor public PendingIntentActivityWrapper(android.content.Context, int, android.content.Intent, int, boolean);
+ ctor public PendingIntentActivityWrapper(android.content.Context, int, android.content.Intent, int, android.os.Bundle?, boolean);
+ method public android.content.Context getContext();
+ method public int getFlags();
+ method public android.content.Intent getIntent();
+ method public android.os.Bundle getOptions();
+ method public android.app.PendingIntent? getPendingIntent();
+ method public int getRequestCode();
+ method public boolean isMutable();
+ }
+
+ public class TileServiceCompat {
+ method public static void startActivityAndCollapse(android.service.quicksettings.TileService, androidx.core.service.quicksettings.PendingIntentActivityWrapper);
+ }
+
+}
+
package androidx.core.telephony {
@RequiresApi(22) public class SubscriptionManagerCompat {
@@ -2194,6 +2224,66 @@
method public static boolean addLinks(android.text.Spannable, java.util.regex.Pattern, String?, String![]?, android.text.util.Linkify.MatchFilter?, android.text.util.Linkify.TransformFilter?);
}
+ @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public final class LocalePreferences {
+ method public static String getCalendarType();
+ method public static String getCalendarType(java.util.Locale);
+ method public static String getCalendarType(boolean);
+ method public static String getCalendarType(java.util.Locale, boolean);
+ method public static String getFirstDayOfWeek();
+ method public static String getFirstDayOfWeek(java.util.Locale);
+ method public static String getFirstDayOfWeek(boolean);
+ method public static String getFirstDayOfWeek(java.util.Locale, boolean);
+ method public static String getHourCycle();
+ method public static String getHourCycle(java.util.Locale);
+ method public static String getHourCycle(boolean);
+ method public static String getHourCycle(java.util.Locale, boolean);
+ method public static String getTemperatureUnit();
+ method public static String getTemperatureUnit(java.util.Locale);
+ method public static String getTemperatureUnit(boolean);
+ method public static String getTemperatureUnit(java.util.Locale, boolean);
+ }
+
+ public static class LocalePreferences.CalendarType {
+ field public static final String CHINESE = "chinese";
+ field public static final String DANGI = "dangi";
+ field public static final String DEFAULT = "";
+ field public static final String GREGORIAN = "gregorian";
+ field public static final String HEBREW = "hebrew";
+ field public static final String INDIAN = "indian";
+ field public static final String ISLAMIC = "islamic";
+ field public static final String ISLAMIC_CIVIL = "islamic-civil";
+ field public static final String ISLAMIC_RGSA = "islamic-rgsa";
+ field public static final String ISLAMIC_TBLA = "islamic-tbla";
+ field public static final String ISLAMIC_UMALQURA = "islamic-umalqura";
+ field public static final String PERSIAN = "persian";
+ }
+
+ public static class LocalePreferences.FirstDayOfWeek {
+ field public static final String DEFAULT = "";
+ field public static final String FRIDAY = "fri";
+ field public static final String MONDAY = "mon";
+ field public static final String SATURDAY = "sat";
+ field public static final String SUNDAY = "sun";
+ field public static final String THURSDAY = "thu";
+ field public static final String TUESDAY = "tue";
+ field public static final String WEDNESDAY = "wed";
+ }
+
+ public static class LocalePreferences.HourCycle {
+ field public static final String DEFAULT = "";
+ field public static final String H11 = "h11";
+ field public static final String H12 = "h12";
+ field public static final String H23 = "h23";
+ field public static final String H24 = "h24";
+ }
+
+ public static class LocalePreferences.TemperatureUnit {
+ field public static final String CELSIUS = "celsius";
+ field public static final String DEFAULT = "";
+ field public static final String FAHRENHEIT = "fahrenhe";
+ field public static final String KELVIN = "kelvin";
+ }
+
}
package androidx.core.util {
@@ -2213,6 +2303,10 @@
method public void accept(T!);
}
+ @java.lang.FunctionalInterface public interface Function<T, R> {
+ method public R! apply(T!);
+ }
+
public class ObjectsCompat {
method public static boolean equals(Object?, Object?);
method public static int hash(java.lang.Object!...);
@@ -2275,6 +2369,14 @@
method public T! get();
}
+ public class TypedValueCompat {
+ method public static float deriveDimension(int, float, android.util.DisplayMetrics);
+ method public static float dpToPx(float, android.util.DisplayMetrics);
+ method public static float pxToDp(float, android.util.DisplayMetrics);
+ method public static float pxToSp(float, android.util.DisplayMetrics);
+ method public static float spToPx(float, android.util.DisplayMetrics);
+ }
+
}
package androidx.core.view {
@@ -2738,9 +2840,12 @@
method public void setSupportBackgroundTintMode(android.graphics.PorterDuff.Mode?);
}
- @Deprecated public final class VelocityTrackerCompat {
+ public final class VelocityTrackerCompat {
+ method public static float getAxisVelocity(android.view.VelocityTracker, int);
+ method public static float getAxisVelocity(android.view.VelocityTracker, int, int);
method @Deprecated public static float getXVelocity(android.view.VelocityTracker!, int);
method @Deprecated public static float getYVelocity(android.view.VelocityTracker!, int);
+ method public static boolean isAxisSupported(android.view.VelocityTracker, int);
}
public class ViewCompat {
@@ -3263,6 +3368,7 @@
field @Deprecated public static final int TYPE_VIEW_HOVER_ENTER = 128; // 0x80
field @Deprecated public static final int TYPE_VIEW_HOVER_EXIT = 256; // 0x100
field @Deprecated public static final int TYPE_VIEW_SCROLLED = 4096; // 0x1000
+ field public static final int TYPE_VIEW_TARGETED_BY_SCROLL = 67108864; // 0x4000000
field @Deprecated public static final int TYPE_VIEW_TEXT_SELECTION_CHANGED = 8192; // 0x2000
field public static final int TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 131072; // 0x20000
field public static final int TYPE_WINDOWS_CHANGED = 4194304; // 0x400000
@@ -3445,6 +3551,7 @@
method public static androidx.core.view.accessibility.AccessibilityNodeInfoCompat! wrap(android.view.accessibility.AccessibilityNodeInfo);
field public static final int ACTION_ACCESSIBILITY_FOCUS = 64; // 0x40
field public static final String ACTION_ARGUMENT_COLUMN_INT = "android.view.accessibility.action.ARGUMENT_COLUMN_INT";
+ field public static final String ACTION_ARGUMENT_DIRECTION_INT = "androidx.core.view.accessibility.action.ARGUMENT_DIRECTION_INT";
field public static final String ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN = "ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN";
field public static final String ACTION_ARGUMENT_HTML_ELEMENT_STRING = "ACTION_ARGUMENT_HTML_ELEMENT_STRING";
field public static final String ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT = "ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT";
@@ -3526,6 +3633,7 @@
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_BACKWARD;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_DOWN;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_FORWARD;
+ field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_SCROLL_IN_DIRECTION;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_LEFT;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_RIGHT;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_TO_POSITION;
@@ -3698,6 +3806,7 @@
}
public class AccessibilityWindowInfoCompat {
+ ctor public AccessibilityWindowInfoCompat();
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat? getAnchor();
method public void getBoundsInScreen(android.graphics.Rect);
method public androidx.core.view.accessibility.AccessibilityWindowInfoCompat? getChild(int);
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 7c8a63c..6c10f64 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -156,6 +156,15 @@
field public static final int TOTAL_INDEX = 0; // 0x0
}
+ public final class GrammaticalInflectionManagerCompat {
+ method @AnyThread public static int getApplicationGrammaticalGender(android.content.Context);
+ method @AnyThread public static void setRequestedApplicationGrammaticalGender(android.content.Context, int);
+ field public static final int GRAMMATICAL_GENDER_FEMININE = 2; // 0x2
+ field public static final int GRAMMATICAL_GENDER_MASCULINE = 3; // 0x3
+ field public static final int GRAMMATICAL_GENDER_NEUTRAL = 1; // 0x1
+ field public static final int GRAMMATICAL_GENDER_NOT_SPECIFIED = 0; // 0x0
+ }
+
@Deprecated public abstract class JobIntentService extends android.app.Service {
ctor @Deprecated public JobIntentService();
method @Deprecated public static void enqueueWork(android.content.Context, Class<?>, int, android.content.Intent);
@@ -793,6 +802,7 @@
public final class NotificationManagerCompat {
method public boolean areNotificationsEnabled();
+ method public boolean canUseFullScreenIntent();
method public void cancel(int);
method public void cancel(String?, int);
method public void cancelAll();
@@ -953,6 +963,7 @@
}
public final class ServiceCompat {
+ method public static void startForeground(android.app.Service, int, android.app.Notification, int);
method public static void stopForeground(android.app.Service, int);
field public static final int START_STICKY = 1; // 0x1
field public static final int STOP_FOREGROUND_DETACH = 2; // 0x2
@@ -1104,7 +1115,6 @@
ctor protected FileProvider(@XmlRes int);
method public int delete(android.net.Uri, String?, String![]?);
method public String? getType(android.net.Uri);
- method public String? getTypeAnonymous(android.net.Uri);
method public static android.net.Uri! getUriForFile(android.content.Context, String, java.io.File);
method public static android.net.Uri getUriForFile(android.content.Context, String, java.io.File, String);
method public android.net.Uri! insert(android.net.Uri, android.content.ContentValues);
@@ -2053,6 +2063,26 @@
}
+package androidx.core.service.quicksettings {
+
+ public class PendingIntentActivityWrapper {
+ ctor public PendingIntentActivityWrapper(android.content.Context, int, android.content.Intent, int, boolean);
+ ctor public PendingIntentActivityWrapper(android.content.Context, int, android.content.Intent, int, android.os.Bundle?, boolean);
+ method public android.content.Context getContext();
+ method public int getFlags();
+ method public android.content.Intent getIntent();
+ method public android.os.Bundle getOptions();
+ method public android.app.PendingIntent? getPendingIntent();
+ method public int getRequestCode();
+ method public boolean isMutable();
+ }
+
+ public class TileServiceCompat {
+ method public static void startActivityAndCollapse(android.service.quicksettings.TileService, androidx.core.service.quicksettings.PendingIntentActivityWrapper);
+ }
+
+}
+
package androidx.core.telephony {
@RequiresApi(22) public class SubscriptionManagerCompat {
@@ -2201,6 +2231,66 @@
method public static boolean addLinks(android.text.Spannable, java.util.regex.Pattern, String?, String![]?, android.text.util.Linkify.MatchFilter?, android.text.util.Linkify.TransformFilter?);
}
+ @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public final class LocalePreferences {
+ method public static String getCalendarType();
+ method public static String getCalendarType(java.util.Locale);
+ method public static String getCalendarType(boolean);
+ method public static String getCalendarType(java.util.Locale, boolean);
+ method public static String getFirstDayOfWeek();
+ method public static String getFirstDayOfWeek(java.util.Locale);
+ method public static String getFirstDayOfWeek(boolean);
+ method public static String getFirstDayOfWeek(java.util.Locale, boolean);
+ method public static String getHourCycle();
+ method public static String getHourCycle(java.util.Locale);
+ method public static String getHourCycle(boolean);
+ method public static String getHourCycle(java.util.Locale, boolean);
+ method public static String getTemperatureUnit();
+ method public static String getTemperatureUnit(java.util.Locale);
+ method public static String getTemperatureUnit(boolean);
+ method public static String getTemperatureUnit(java.util.Locale, boolean);
+ }
+
+ public static class LocalePreferences.CalendarType {
+ field public static final String CHINESE = "chinese";
+ field public static final String DANGI = "dangi";
+ field public static final String DEFAULT = "";
+ field public static final String GREGORIAN = "gregorian";
+ field public static final String HEBREW = "hebrew";
+ field public static final String INDIAN = "indian";
+ field public static final String ISLAMIC = "islamic";
+ field public static final String ISLAMIC_CIVIL = "islamic-civil";
+ field public static final String ISLAMIC_RGSA = "islamic-rgsa";
+ field public static final String ISLAMIC_TBLA = "islamic-tbla";
+ field public static final String ISLAMIC_UMALQURA = "islamic-umalqura";
+ field public static final String PERSIAN = "persian";
+ }
+
+ public static class LocalePreferences.FirstDayOfWeek {
+ field public static final String DEFAULT = "";
+ field public static final String FRIDAY = "fri";
+ field public static final String MONDAY = "mon";
+ field public static final String SATURDAY = "sat";
+ field public static final String SUNDAY = "sun";
+ field public static final String THURSDAY = "thu";
+ field public static final String TUESDAY = "tue";
+ field public static final String WEDNESDAY = "wed";
+ }
+
+ public static class LocalePreferences.HourCycle {
+ field public static final String DEFAULT = "";
+ field public static final String H11 = "h11";
+ field public static final String H12 = "h12";
+ field public static final String H23 = "h23";
+ field public static final String H24 = "h24";
+ }
+
+ public static class LocalePreferences.TemperatureUnit {
+ field public static final String CELSIUS = "celsius";
+ field public static final String DEFAULT = "";
+ field public static final String FAHRENHEIT = "fahrenhe";
+ field public static final String KELVIN = "kelvin";
+ }
+
}
package androidx.core.util {
@@ -2220,6 +2310,10 @@
method public void accept(T!);
}
+ @java.lang.FunctionalInterface public interface Function<T, R> {
+ method public R! apply(T!);
+ }
+
public class ObjectsCompat {
method public static boolean equals(Object?, Object?);
method public static int hash(java.lang.Object!...);
@@ -2282,6 +2376,14 @@
method public T! get();
}
+ public class TypedValueCompat {
+ method public static float deriveDimension(int, float, android.util.DisplayMetrics);
+ method public static float dpToPx(float, android.util.DisplayMetrics);
+ method public static float pxToDp(float, android.util.DisplayMetrics);
+ method public static float pxToSp(float, android.util.DisplayMetrics);
+ method public static float spToPx(float, android.util.DisplayMetrics);
+ }
+
}
package androidx.core.view {
@@ -2745,9 +2847,12 @@
method public void setSupportBackgroundTintMode(android.graphics.PorterDuff.Mode?);
}
- @Deprecated public final class VelocityTrackerCompat {
+ public final class VelocityTrackerCompat {
+ method public static float getAxisVelocity(android.view.VelocityTracker, int);
+ method public static float getAxisVelocity(android.view.VelocityTracker, int, int);
method @Deprecated public static float getXVelocity(android.view.VelocityTracker!, int);
method @Deprecated public static float getYVelocity(android.view.VelocityTracker!, int);
+ method public static boolean isAxisSupported(android.view.VelocityTracker, int);
}
public class ViewCompat {
@@ -3270,6 +3375,7 @@
field @Deprecated public static final int TYPE_VIEW_HOVER_ENTER = 128; // 0x80
field @Deprecated public static final int TYPE_VIEW_HOVER_EXIT = 256; // 0x100
field @Deprecated public static final int TYPE_VIEW_SCROLLED = 4096; // 0x1000
+ field public static final int TYPE_VIEW_TARGETED_BY_SCROLL = 67108864; // 0x4000000
field @Deprecated public static final int TYPE_VIEW_TEXT_SELECTION_CHANGED = 8192; // 0x2000
field public static final int TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 131072; // 0x20000
field public static final int TYPE_WINDOWS_CHANGED = 4194304; // 0x400000
@@ -3452,6 +3558,7 @@
method public static androidx.core.view.accessibility.AccessibilityNodeInfoCompat! wrap(android.view.accessibility.AccessibilityNodeInfo);
field public static final int ACTION_ACCESSIBILITY_FOCUS = 64; // 0x40
field public static final String ACTION_ARGUMENT_COLUMN_INT = "android.view.accessibility.action.ARGUMENT_COLUMN_INT";
+ field public static final String ACTION_ARGUMENT_DIRECTION_INT = "androidx.core.view.accessibility.action.ARGUMENT_DIRECTION_INT";
field public static final String ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN = "ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN";
field public static final String ACTION_ARGUMENT_HTML_ELEMENT_STRING = "ACTION_ARGUMENT_HTML_ELEMENT_STRING";
field public static final String ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT = "ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT";
@@ -3533,6 +3640,7 @@
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_BACKWARD;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_DOWN;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_FORWARD;
+ field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_SCROLL_IN_DIRECTION;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_LEFT;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_RIGHT;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_TO_POSITION;
@@ -3705,6 +3813,7 @@
}
public class AccessibilityWindowInfoCompat {
+ ctor public AccessibilityWindowInfoCompat();
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat? getAnchor();
method public void getBoundsInScreen(android.graphics.Rect);
method public androidx.core.view.accessibility.AccessibilityWindowInfoCompat? getChild(int);
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 945e47f..828fa29 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -198,6 +198,15 @@
@IntDef(flag=true, value={androidx.core.app.FrameMetricsAggregator.TOTAL_DURATION, androidx.core.app.FrameMetricsAggregator.INPUT_DURATION, androidx.core.app.FrameMetricsAggregator.LAYOUT_MEASURE_DURATION, androidx.core.app.FrameMetricsAggregator.DRAW_DURATION, androidx.core.app.FrameMetricsAggregator.SYNC_DURATION, androidx.core.app.FrameMetricsAggregator.COMMAND_DURATION, androidx.core.app.FrameMetricsAggregator.SWAP_DURATION, androidx.core.app.FrameMetricsAggregator.DELAY_DURATION, androidx.core.app.FrameMetricsAggregator.ANIMATION_DURATION, androidx.core.app.FrameMetricsAggregator.EVERY_DURATION}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface FrameMetricsAggregator.MetricType {
}
+ public final class GrammaticalInflectionManagerCompat {
+ method @AnyThread public static int getApplicationGrammaticalGender(android.content.Context);
+ method @AnyThread public static void setRequestedApplicationGrammaticalGender(android.content.Context, int);
+ field public static final int GRAMMATICAL_GENDER_FEMININE = 2; // 0x2
+ field public static final int GRAMMATICAL_GENDER_MASCULINE = 3; // 0x3
+ field public static final int GRAMMATICAL_GENDER_NEUTRAL = 1; // 0x1
+ field public static final int GRAMMATICAL_GENDER_NOT_SPECIFIED = 0; // 0x0
+ }
+
@Deprecated public abstract class JobIntentService extends android.app.Service {
ctor @Deprecated public JobIntentService();
method @Deprecated public static void enqueueWork(android.content.Context, Class<?>, int, android.content.Intent);
@@ -886,6 +895,7 @@
public final class NotificationManagerCompat {
method public boolean areNotificationsEnabled();
+ method public boolean canUseFullScreenIntent();
method public void cancel(int);
method public void cancel(String?, int);
method public void cancelAll();
@@ -1067,6 +1077,7 @@
}
public final class ServiceCompat {
+ method public static void startForeground(android.app.Service, int, android.app.Notification, int);
method public static void stopForeground(android.app.Service, @androidx.core.app.ServiceCompat.StopForegroundFlags int);
field public static final int START_STICKY = 1; // 0x1
field public static final int STOP_FOREGROUND_DETACH = 2; // 0x2
@@ -1221,7 +1232,6 @@
ctor protected FileProvider(@XmlRes int);
method public int delete(android.net.Uri, String?, String![]?);
method public String? getType(android.net.Uri);
- method public String? getTypeAnonymous(android.net.Uri);
method public static android.net.Uri! getUriForFile(android.content.Context, String, java.io.File);
method public static android.net.Uri getUriForFile(android.content.Context, String, java.io.File, String);
method public android.net.Uri! insert(android.net.Uri, android.content.ContentValues);
@@ -2418,6 +2428,26 @@
}
+package androidx.core.service.quicksettings {
+
+ public class PendingIntentActivityWrapper {
+ ctor public PendingIntentActivityWrapper(android.content.Context, int, android.content.Intent, int, boolean);
+ ctor public PendingIntentActivityWrapper(android.content.Context, int, android.content.Intent, int, android.os.Bundle?, boolean);
+ method public android.content.Context getContext();
+ method public int getFlags();
+ method public android.content.Intent getIntent();
+ method public android.os.Bundle getOptions();
+ method public android.app.PendingIntent? getPendingIntent();
+ method public int getRequestCode();
+ method public boolean isMutable();
+ }
+
+ public class TileServiceCompat {
+ method public static void startActivityAndCollapse(android.service.quicksettings.TileService, androidx.core.service.quicksettings.PendingIntentActivityWrapper);
+ }
+
+}
+
package androidx.core.telephony {
@RequiresApi(22) public class SubscriptionManagerCompat {
@@ -2571,6 +2601,66 @@
@IntDef(flag=true, value={android.text.util.Linkify.WEB_URLS, android.text.util.Linkify.EMAIL_ADDRESSES, android.text.util.Linkify.PHONE_NUMBERS, android.text.util.Linkify.MAP_ADDRESSES, android.text.util.Linkify.ALL}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface LinkifyCompat.LinkifyMask {
}
+ @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public final class LocalePreferences {
+ method public static String getCalendarType();
+ method public static String getCalendarType(java.util.Locale);
+ method public static String getCalendarType(boolean);
+ method public static String getCalendarType(java.util.Locale, boolean);
+ method public static String getFirstDayOfWeek();
+ method public static String getFirstDayOfWeek(java.util.Locale);
+ method public static String getFirstDayOfWeek(boolean);
+ method public static String getFirstDayOfWeek(java.util.Locale, boolean);
+ method public static String getHourCycle();
+ method public static String getHourCycle(java.util.Locale);
+ method public static String getHourCycle(boolean);
+ method public static String getHourCycle(java.util.Locale, boolean);
+ method public static String getTemperatureUnit();
+ method public static String getTemperatureUnit(java.util.Locale);
+ method public static String getTemperatureUnit(boolean);
+ method public static String getTemperatureUnit(java.util.Locale, boolean);
+ }
+
+ public static class LocalePreferences.CalendarType {
+ field public static final String CHINESE = "chinese";
+ field public static final String DANGI = "dangi";
+ field public static final String DEFAULT = "";
+ field public static final String GREGORIAN = "gregorian";
+ field public static final String HEBREW = "hebrew";
+ field public static final String INDIAN = "indian";
+ field public static final String ISLAMIC = "islamic";
+ field public static final String ISLAMIC_CIVIL = "islamic-civil";
+ field public static final String ISLAMIC_RGSA = "islamic-rgsa";
+ field public static final String ISLAMIC_TBLA = "islamic-tbla";
+ field public static final String ISLAMIC_UMALQURA = "islamic-umalqura";
+ field public static final String PERSIAN = "persian";
+ }
+
+ public static class LocalePreferences.FirstDayOfWeek {
+ field public static final String DEFAULT = "";
+ field public static final String FRIDAY = "fri";
+ field public static final String MONDAY = "mon";
+ field public static final String SATURDAY = "sat";
+ field public static final String SUNDAY = "sun";
+ field public static final String THURSDAY = "thu";
+ field public static final String TUESDAY = "tue";
+ field public static final String WEDNESDAY = "wed";
+ }
+
+ public static class LocalePreferences.HourCycle {
+ field public static final String DEFAULT = "";
+ field public static final String H11 = "h11";
+ field public static final String H12 = "h12";
+ field public static final String H23 = "h23";
+ field public static final String H24 = "h24";
+ }
+
+ public static class LocalePreferences.TemperatureUnit {
+ field public static final String CELSIUS = "celsius";
+ field public static final String DEFAULT = "";
+ field public static final String FAHRENHEIT = "fahrenhe";
+ field public static final String KELVIN = "kelvin";
+ }
+
}
package androidx.core.util {
@@ -2594,6 +2684,10 @@
method public static void buildShortClassTag(Object!, StringBuilder!);
}
+ @java.lang.FunctionalInterface public interface Function<T, R> {
+ method public R! apply(T!);
+ }
+
@Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class LogWriter extends java.io.Writer {
ctor @Deprecated public LogWriter(String!);
method @Deprecated public void close();
@@ -2694,6 +2788,14 @@
field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final int HUNDRED_DAY_FIELD_LEN = 19; // 0x13
}
+ public class TypedValueCompat {
+ method public static float deriveDimension(int, float, android.util.DisplayMetrics);
+ method public static float dpToPx(float, android.util.DisplayMetrics);
+ method public static float pxToDp(float, android.util.DisplayMetrics);
+ method public static float pxToSp(float, android.util.DisplayMetrics);
+ method public static float spToPx(float, android.util.DisplayMetrics);
+ }
+
}
package androidx.core.view {
@@ -3182,9 +3284,15 @@
method public void setSupportBackgroundTintMode(android.graphics.PorterDuff.Mode?);
}
- @Deprecated public final class VelocityTrackerCompat {
+ public final class VelocityTrackerCompat {
+ method public static float getAxisVelocity(android.view.VelocityTracker, @androidx.core.view.VelocityTrackerCompat.VelocityTrackableMotionEventAxis int);
+ method public static float getAxisVelocity(android.view.VelocityTracker, @androidx.core.view.VelocityTrackerCompat.VelocityTrackableMotionEventAxis int, int);
method @Deprecated public static float getXVelocity(android.view.VelocityTracker!, int);
method @Deprecated public static float getYVelocity(android.view.VelocityTracker!, int);
+ method public static boolean isAxisSupported(android.view.VelocityTracker, @androidx.core.view.VelocityTrackerCompat.VelocityTrackableMotionEventAxis int);
+ }
+
+ @IntDef({android.view.MotionEvent.AXIS_X, android.view.MotionEvent.AXIS_Y, android.view.MotionEvent.AXIS_SCROLL}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface VelocityTrackerCompat.VelocityTrackableMotionEventAxis {
}
public class ViewCompat {
@@ -3733,6 +3841,7 @@
field @Deprecated public static final int TYPE_VIEW_HOVER_ENTER = 128; // 0x80
field @Deprecated public static final int TYPE_VIEW_HOVER_EXIT = 256; // 0x100
field @Deprecated public static final int TYPE_VIEW_SCROLLED = 4096; // 0x1000
+ field public static final int TYPE_VIEW_TARGETED_BY_SCROLL = 67108864; // 0x4000000
field @Deprecated public static final int TYPE_VIEW_TEXT_SELECTION_CHANGED = 8192; // 0x2000
field public static final int TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 131072; // 0x20000
field public static final int TYPE_WINDOWS_CHANGED = 4194304; // 0x400000
@@ -3920,6 +4029,7 @@
method public static androidx.core.view.accessibility.AccessibilityNodeInfoCompat! wrap(android.view.accessibility.AccessibilityNodeInfo);
field public static final int ACTION_ACCESSIBILITY_FOCUS = 64; // 0x40
field public static final String ACTION_ARGUMENT_COLUMN_INT = "android.view.accessibility.action.ARGUMENT_COLUMN_INT";
+ field public static final String ACTION_ARGUMENT_DIRECTION_INT = "androidx.core.view.accessibility.action.ARGUMENT_DIRECTION_INT";
field public static final String ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN = "ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN";
field public static final String ACTION_ARGUMENT_HTML_ELEMENT_STRING = "ACTION_ARGUMENT_HTML_ELEMENT_STRING";
field public static final String ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT = "ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT";
@@ -4005,6 +4115,7 @@
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_BACKWARD;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_DOWN;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_FORWARD;
+ field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_SCROLL_IN_DIRECTION;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_LEFT;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_RIGHT;
field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_SCROLL_TO_POSITION;
@@ -4179,6 +4290,7 @@
}
public class AccessibilityWindowInfoCompat {
+ ctor public AccessibilityWindowInfoCompat();
method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat? getAnchor();
method public void getBoundsInScreen(android.graphics.Rect);
method public androidx.core.view.accessibility.AccessibilityWindowInfoCompat? getChild(int);
diff --git a/core/core/build.gradle b/core/core/build.gradle
index fa351a1..149662a 100644
--- a/core/core/build.gradle
+++ b/core/core/build.gradle
@@ -22,6 +22,9 @@
implementation("androidx.concurrent:concurrent-futures:1.0.0")
implementation("androidx.interpolator:interpolator:1.0.0")
+ // Workaround for Kotlin dependency constraints
+ implementation(libs.kotlinStdlib)
+
// We don't ship this as a public artifact, so it must remain a project-type dependency.
annotationProcessor(projectOrArtifact(":versionedparcelable:versionedparcelable-compiler"))
@@ -90,10 +93,11 @@
}
androidx {
- name = "Android Support Library compat"
+ name = "Core"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.CORE
inceptionYear = "2015"
- description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\'t a part of the framework APIs. Compatible on devices running API 14 or later."
+ description = "Provides backward-compatible implementations of Android platform APIs and " +
+ "features."
failOnDeprecationWarnings = false
}
diff --git a/core/core/lint-baseline.xml b/core/core/lint-baseline.xml
index dc58fea..4427044 100644
--- a/core/core/lint-baseline.xml
+++ b/core/core/lint-baseline.xml
@@ -722,6 +722,15 @@
</issue>
<issue
+ id="Range"
+ message="Value must be ≥ 1 and ≤ 200 but `getSvid` can be 206"
+ errorLine1=" return mWrapped.getSvid(satelliteIndex);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/location/GnssStatusWrapper.java"/>
+ </issue>
+
+ <issue
id="WrongConstant"
message="Must be one of: Callback.DISPATCH_MODE_STOP, Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE"
errorLine1=" super(compat.getDispatchMode());"
@@ -760,6 +769,42 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
+ errorLine1=" public @interface HourCycleTypes {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/text/util/LocalePreferences.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" public @interface CalendarTypes {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/text/util/LocalePreferences.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" public @interface TemperatureUnits {"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/text/util/LocalePreferences.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" public @interface Days {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/core/text/util/LocalePreferences.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
errorLine1=" public static final class TvExtender implements Extender {"
errorLine2=" ~~~~~~~~~~">
<location
@@ -821,6 +866,33 @@
</issue>
<issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" public static void setTileServiceWrapper(@NonNull TileServiceWrapper serviceWrapper) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/service/quicksettings/TileServiceCompat.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" public static void clearTileServiceWrapper() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/service/quicksettings/TileServiceCompat.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" public @interface VelocityTrackableMotionEventAxis {}"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/view/VelocityTrackerCompat.java"/>
+ </issue>
+
+ <issue
id="BanThreadSleep"
message="Uses Thread.sleep()"
errorLine1=" Thread.sleep(timeSliceMs);"
@@ -2136,6 +2208,15 @@
<issue
id="ClassVerificationFailure"
+ message="This call references a method added in API level 23; however, the containing class androidx.core.app.NotificationManagerCompat is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" mContext.checkSelfPermission(Manifest.permission.USE_FULL_SCREEN_INTENT);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/core/app/NotificationManagerCompat.java"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
message="This call references a method added in API level 28; however, the containing class androidx.core.text.PrecomputedTextCompat.Params is reachable from earlier API levels and will fail run-time class verification."
errorLine1=" mWrapped = new PrecomputedText.Params.Builder(paint)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
diff --git a/core/core/src/androidTest/AndroidManifest.xml b/core/core/src/androidTest/AndroidManifest.xml
index 2688f36..30756c9 100644
--- a/core/core/src/androidTest/AndroidManifest.xml
+++ b/core/core/src/androidTest/AndroidManifest.xml
@@ -144,6 +144,11 @@
android:name="androidx.core.view.inputmethod.ImeSecondarySplitTestActivity"
android:exported="true" />
+ <activity
+ android:name="androidx.core.app.GrammaticalInfectionActivity"
+ android:configChanges="grammaticalGender"
+ android:exported="true" />
+
<activity-alias
android:name="androidx.core.app.NavUtilsAliasActivity"
android:targetActivity="androidx.core.app.NavUtilsActivity">
diff --git a/core/core/src/androidTest/java/androidx/core/app/GrammaticalInfectionActivity.java b/core/core/src/androidTest/java/androidx/core/app/GrammaticalInfectionActivity.java
new file mode 100644
index 0000000..07e58d1
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/app/GrammaticalInfectionActivity.java
@@ -0,0 +1,49 @@
+/*
+ * 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.core.app;
+
+import android.app.Activity;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class GrammaticalInfectionActivity extends Activity {
+ private final CountDownLatch mCountDownLatch = new CountDownLatch(1);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(androidx.core.test.R.layout.activity_compat_activity);
+ }
+
+ @Override
+ public void onConfigurationChanged(@NonNull Configuration config) {
+ super.onConfigurationChanged(config);
+ mCountDownLatch.countDown();
+ }
+
+ public void await() throws InterruptedException {
+ mCountDownLatch.await(5, TimeUnit.SECONDS);
+ }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/app/GrammaticalInflectionManagerCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/GrammaticalInflectionManagerCompatTest.java
new file mode 100644
index 0000000..187ca12
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/app/GrammaticalInflectionManagerCompatTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.core.app;
+
+import static androidx.core.app.GrammaticalInflectionManagerCompat.GRAMMATICAL_GENDER_MASCULINE;
+
+import static org.junit.Assert.assertEquals;
+
+import android.support.v4.BaseInstrumentationTestCase;
+
+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;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class GrammaticalInflectionManagerCompatTest extends
+ BaseInstrumentationTestCase<GrammaticalInfectionActivity> {
+
+ public GrammaticalInflectionManagerCompatTest() {
+ super(GrammaticalInfectionActivity.class);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 34)
+ public void testSetGrammaticalGender() throws InterruptedException {
+ GrammaticalInflectionManagerCompat.setRequestedApplicationGrammaticalGender(
+ mActivityTestRule.getActivity(),
+ GRAMMATICAL_GENDER_MASCULINE
+ );
+
+ mActivityTestRule.getActivity().await();
+
+ assertEquals(GRAMMATICAL_GENDER_MASCULINE,
+ GrammaticalInflectionManagerCompat.getApplicationGrammaticalGender(
+ mActivityTestRule.getActivity()));
+ }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/app/NotificationManagerCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/NotificationManagerCompatTest.java
index 6531103..a264df7 100644
--- a/core/core/src/androidTest/java/androidx/core/app/NotificationManagerCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/NotificationManagerCompatTest.java
@@ -26,7 +26,10 @@
import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_MAX;
import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_MIN;
+import static org.mockito.Mockito.spy;
+
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -34,6 +37,7 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import android.Manifest;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
@@ -1159,6 +1163,27 @@
}
@Test
+ public void testCanUseFullScreenIntent() {
+ NotificationManager fakeManager = mock(NotificationManager.class);
+
+ Context spyContext = spy(mContext);
+
+ NotificationManagerCompat notificationManagerCompat =
+ new NotificationManagerCompat(fakeManager, spyContext);
+
+ final boolean canUse = notificationManagerCompat.canUseFullScreenIntent();
+
+ if (Build.VERSION.SDK_INT < 29) {
+ assertTrue(canUse);
+
+ } else if (Build.VERSION.SDK_INT < 34) {
+ verify(spyContext, times(1))
+ .checkSelfPermission(Manifest.permission.USE_FULL_SCREEN_INTENT);
+ } else {
+ verify(fakeManager, times(1)).canUseFullScreenIntent();
+ }
+ }
+
public void testGetActiveNotifications() {
NotificationManager fakeManager = mock(NotificationManager.class);
NotificationManagerCompat notificationManager =
diff --git a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
index 03b73b5..b510825 100644
--- a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
@@ -87,6 +87,7 @@
import android.app.KeyguardManager;
import android.app.NotificationManager;
import android.app.SearchManager;
+import android.app.UiAutomation;
import android.app.UiModeManager;
import android.app.WallpaperManager;
import android.app.admin.DevicePolicyManager;
@@ -521,11 +522,15 @@
@Test
@SdkSuppress(minSdkVersion = 29, maxSdkVersion = 32)
public void testRegisterReceiverPermissionNotGrantedApi26() {
- InstrumentationRegistry
- .getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
- assertThrows(RuntimeException.class,
- () -> ContextCompat.registerReceiver(mContext,
- mTestReceiver, mTestFilter, ContextCompat.RECEIVER_NOT_EXPORTED));
+ UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ uiAutomation.adoptShellPermissionIdentity();
+ try {
+ assertThrows(RuntimeException.class,
+ () -> ContextCompat.registerReceiver(mContext,
+ mTestReceiver, mTestFilter, ContextCompat.RECEIVER_NOT_EXPORTED));
+ } finally {
+ uiAutomation.dropShellPermissionIdentity();
+ }
}
@Test
diff --git a/core/core/src/androidTest/java/androidx/core/service/quicksettings/TileServiceCompatTest.java b/core/core/src/androidTest/java/androidx/core/service/quicksettings/TileServiceCompatTest.java
new file mode 100644
index 0000000..a78f703
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/service/quicksettings/TileServiceCompatTest.java
@@ -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.core.service.quicksettings;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.service.quicksettings.TileService;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit test for {@link TileServiceCompat}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class TileServiceCompatTest {
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+
+ @After
+ public void tearDown() {
+ TileServiceCompat.clearTileServiceWrapper();
+ }
+
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ public void startActivityAndCollapse_usesPendingIntent() {
+ TileServiceCompat.TileServiceWrapper tileServiceWrapper =
+ mock(TileServiceCompat.TileServiceWrapper.class);
+ TileService tileService = mock(TileService.class);
+ int requestCode = 7465;
+ Intent intent = new Intent();
+ Bundle options = new Bundle();
+ PendingIntentActivityWrapper wrapper = new PendingIntentActivityWrapper(mContext,
+ requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT, options, /* isMutable = */
+ true);
+ TileServiceCompat.setTileServiceWrapper(tileServiceWrapper);
+
+ TileServiceCompat.startActivityAndCollapse(tileService, wrapper);
+
+ verify(tileServiceWrapper).startActivityAndCollapse(wrapper.getPendingIntent());
+ }
+
+ @SdkSuppress(minSdkVersion = 24, maxSdkVersion = 33)
+ @Test
+ public void startActivityAndCollapse_usesIntent() {
+ TileServiceCompat.TileServiceWrapper tileServiceWrapper =
+ mock(TileServiceCompat.TileServiceWrapper.class);
+ TileService tileService = mock(TileService.class);
+ int requestCode = 7465;
+ Intent intent = new Intent();
+ Bundle options = new Bundle();
+ PendingIntentActivityWrapper wrapper = new PendingIntentActivityWrapper(mContext,
+ requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT, options, /* isMutable = */
+ true);
+ TileServiceCompat.setTileServiceWrapper(tileServiceWrapper);
+
+ TileServiceCompat.startActivityAndCollapse(tileService, wrapper);
+
+ verify(tileServiceWrapper).startActivityAndCollapse(intent);
+ }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/text/util/LocalePreferencesTest.java b/core/core/src/androidTest/java/androidx/core/text/util/LocalePreferencesTest.java
new file mode 100644
index 0000000..a47cc38
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/text/util/LocalePreferencesTest.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 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.core.text.util;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Build.VERSION_CODES;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Locale;
+
+@SmallTest
+@SdkSuppress(minSdkVersion = VERSION_CODES.N)
+@RunWith(AndroidJUnit4.class)
+public class LocalePreferencesTest {
+ private static Locale sLocale;
+
+ @BeforeClass
+ public static void setUpClass() throws Exception {
+ sLocale = Locale.getDefault(Locale.Category.FORMAT);
+ }
+
+ @After
+ public void tearDown() {
+ Locale.setDefault(sLocale);
+ }
+
+ // Hour cycle
+ @Test
+ public void getHourCycle_hasSubTags_resultIsH24() throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+ String result = LocalePreferences.getHourCycle();
+
+ assertEquals(LocalePreferences.HourCycle.H24, result);
+ }
+
+ @Test
+ public void getHourCycle_hasSubTagsWithoutHourCycleTag_resultIsH12() throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-mu-celsius-fw-wed"));
+
+ String result = LocalePreferences.getHourCycle();
+
+ assertEquals(LocalePreferences.HourCycle.H12, result);
+ }
+
+ @Test
+ public void getHourCycle_hasSubTagsAndDisableResolved_resultIsH24() throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+ String result = LocalePreferences.getHourCycle(false);
+
+ assertEquals(LocalePreferences.HourCycle.H24, result);
+ }
+
+ @Test
+ public void getHourCycle_hasSubTagsWithoutHourCycleTagAndDisableResolved_resultIsEmpty()
+ throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-mu-celsius-fw-wed"));
+
+ String result = LocalePreferences.getHourCycle(false);
+
+ assertEquals(LocalePreferences.HourCycle.DEFAULT, result);
+ }
+
+ @Test
+ public void getHourCycle_inputLocaleWithHourCycleTag_resultIsH12() throws Exception {
+ String result = LocalePreferences.getHourCycle(Locale.forLanguageTag("en-US-u-hc-h12"));
+
+ assertEquals(LocalePreferences.HourCycle.H12, result);
+ }
+
+ @Test
+ public void getHourCycle_inputLocaleWithoutHourCycleTag_resultIsH12() throws Exception {
+ String result = LocalePreferences.getHourCycle(Locale.forLanguageTag("en-US"));
+
+ assertEquals(LocalePreferences.HourCycle.H12, result);
+ }
+
+ @Test
+ public void getHourCycle_inputH23Locale_resultIsH23() throws Exception {
+ String result = LocalePreferences.getHourCycle(Locale.forLanguageTag("fr-FR"));
+
+ assertEquals(LocalePreferences.HourCycle.H23, result);
+ }
+
+ @Test
+ public void getHourCycle_inputH23LocaleWithHourCycleTag_resultIsH12() throws Exception {
+ String result = LocalePreferences.getHourCycle(Locale.forLanguageTag("fr-FR-u-hc-h12"));
+
+ assertEquals(LocalePreferences.HourCycle.H12, result);
+ }
+
+ @Test
+ public void getHourCycle_inputLocaleWithoutHourCycleTagAndDisableResolved_resultIsEmpty()
+ throws Exception {
+ String result = LocalePreferences.getHourCycle(Locale.forLanguageTag("en-US"), false);
+
+ assertEquals(LocalePreferences.HourCycle.DEFAULT, result);
+ }
+
+ @Test
+ public void getHourCycle_compareHasResolvedValueIsTrueAndWithoutResolvedValue_sameResult()
+ throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("zh-TW-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+ // Has Hour Cycle subtag
+ String resultWithoutResolvedValue = LocalePreferences.getHourCycle();
+ String resultResolvedIsTrue = LocalePreferences.getHourCycle(true);
+ assertEquals(resultWithoutResolvedValue, resultResolvedIsTrue);
+
+ // Does not have HourCycle subtag
+ Locale.setDefault(Locale.forLanguageTag("zh-TW-u-ca-chinese-mu-celsius-fw-wed"));
+
+ resultWithoutResolvedValue = LocalePreferences.getHourCycle();
+ resultResolvedIsTrue = LocalePreferences.getHourCycle(true);
+ assertEquals(resultWithoutResolvedValue, resultResolvedIsTrue);
+ }
+
+ // Calendar
+ @Test
+ public void getCalendarType_hasSubTags_resultIsChinese() throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+ String result = LocalePreferences.getCalendarType();
+
+ assertEquals(LocalePreferences.CalendarType.CHINESE, result);
+ }
+
+ @Test
+ public void getCalendarType_hasSubTagsWithoutCalendarTag_resultIsGregorian() throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-hc-h24-mu-celsius-fw-wed"));
+
+ String result = LocalePreferences.getCalendarType();
+
+ assertEquals(LocalePreferences.CalendarType.GREGORIAN, result);
+ }
+
+ @Test
+ public void getCalendarType_hasSubTagsAndDisableResolved_resultIsChinese() throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+ String result = LocalePreferences.getCalendarType(false);
+
+ assertEquals(LocalePreferences.CalendarType.CHINESE, result);
+ }
+
+ @Test
+ public void getCalendarType_hasSubTagsWithoutCalendarTagAndDisableResolved_resultIsEmpty()
+ throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-mu-celsius-fw-wed"));
+
+ String result = LocalePreferences.getCalendarType(false);
+
+ assertEquals(LocalePreferences.CalendarType.DEFAULT, result);
+ }
+
+ @Test
+ public void getCalendarType_inputLocaleWithCalendarTag_resultIsChinese() throws Exception {
+ String result =
+ LocalePreferences.getCalendarType(Locale.forLanguageTag("en-US-u-ca-chinese"));
+
+ assertEquals(LocalePreferences.CalendarType.CHINESE, result);
+ }
+
+ @Test
+ public void getCalendarType_inputLocaleWithoutCalendarTag_resultIsGregorian() throws Exception {
+ String result = LocalePreferences.getCalendarType(Locale.forLanguageTag("en-US"));
+
+ assertEquals(LocalePreferences.CalendarType.GREGORIAN, result);
+ }
+
+ @Test
+ public void getCalendarType_inputLocaleWithoutCalendarTagAndDisableResolved_resultIsEmpty()
+ throws Exception {
+ String result = LocalePreferences.getCalendarType(Locale.forLanguageTag("en-US"), false);
+
+ assertEquals(LocalePreferences.CalendarType.DEFAULT, result);
+ }
+
+ // Temperature unit
+ @Test
+ public void getTemperatureUnit_hasSubTags_resultIsCelsius() throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+ String result = LocalePreferences.getTemperatureUnit();
+
+ assertEquals(LocalePreferences.TemperatureUnit.CELSIUS, result);
+ }
+
+ @Test
+ public void getTemperatureUnit_hasSubTagsWithoutUnitTag_resultIsFahrenheit() throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-hc-h24-fw-wed"));
+
+ String result = LocalePreferences.getTemperatureUnit();
+
+ assertEquals(LocalePreferences.TemperatureUnit.FAHRENHEIT, result);
+ }
+
+ @Test
+ public void getTemperatureUnit_hasSubTagsAndDisableResolved_resultIsCelsius() throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+ String result = LocalePreferences.getTemperatureUnit(false);
+
+ assertEquals(LocalePreferences.TemperatureUnit.CELSIUS, result);
+ }
+
+ @Test
+ public void getTemperatureUnit_hasSubTagsAndDisableResolved_resultIsFahrenheit()
+ throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("zh-TW-u-ca-chinese-hc-h24-mu-fahrenhe-fw-wed"));
+
+ String result = LocalePreferences.getTemperatureUnit(false);
+
+ assertEquals(LocalePreferences.TemperatureUnit.FAHRENHEIT, result);
+ }
+
+ @Test
+ public void getTemperatureUnit_hasSubTagsWithoutUnitTagAndDisableResolved_resultIsEmpty()
+ throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-fw-wed"));
+
+ String result = LocalePreferences.getTemperatureUnit(false);
+
+ assertEquals(LocalePreferences.TemperatureUnit.DEFAULT, result);
+ }
+
+ @Test
+ public void getTemperatureUnit_inputLocaleWithUnitTag_resultIsCelsius() throws Exception {
+ String result = LocalePreferences
+ .getTemperatureUnit(Locale.forLanguageTag("en-US-u-mu-celsius"));
+
+ assertEquals(LocalePreferences.TemperatureUnit.CELSIUS, result);
+ }
+
+ @Test
+ public void getTemperatureUnit_inputLocaleWithoutUnitTag_resultIsFahrenheit() throws Exception {
+ String result = LocalePreferences.getTemperatureUnit(Locale.forLanguageTag("en-US"));
+
+ assertEquals(LocalePreferences.TemperatureUnit.FAHRENHEIT, result);
+ }
+
+ @Test
+ public void getTemperatureUnit_inputLocaleWithoutUnitTagAndDisableResolved_resultIsEmpty()
+ throws Exception {
+ String result = LocalePreferences
+ .getTemperatureUnit(Locale.forLanguageTag("en-US"), false);
+
+ assertEquals(LocalePreferences.TemperatureUnit.DEFAULT, result);
+ }
+
+ // First day of week
+ @Test
+ public void getFirstDayOfWeek_hasSubTags_resultIsCelsius() throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+ String result = LocalePreferences.getFirstDayOfWeek();
+
+ assertEquals(LocalePreferences.FirstDayOfWeek.WEDNESDAY, result);
+ }
+
+ @Test
+ public void getFirstDayOfWeek_hasSubTagsWithoutFwTag_resultIsSun() throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-hc-h24"));
+
+ String result = LocalePreferences.getFirstDayOfWeek();
+
+ assertEquals(LocalePreferences.FirstDayOfWeek.SUNDAY, result);
+
+ }
+
+ @Test
+ public void getFirstDayOfWeek_hasSubTagsAndDisableResolved_resultIsWed() throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+ String result = LocalePreferences.getFirstDayOfWeek(false);
+
+ assertEquals(LocalePreferences.FirstDayOfWeek.WEDNESDAY, result);
+ }
+
+ @Test
+ public void getFirstDayOfWeek_hasSubTagsWithoutFwTagAndDisableResolved_resultIsEmpty()
+ throws Exception {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese"));
+
+ String result = LocalePreferences.getFirstDayOfWeek(false);
+
+ assertEquals(LocalePreferences.FirstDayOfWeek.DEFAULT, result);
+ }
+
+ @Test
+ public void getFirstDayOfWeek_inputLocaleWithFwTag_resultIsWed() throws Exception {
+ String result = LocalePreferences
+ .getFirstDayOfWeek(Locale.forLanguageTag("en-US-u-fw-wed"));
+
+ assertEquals(LocalePreferences.FirstDayOfWeek.WEDNESDAY, result);
+ }
+
+ @Test
+ public void getFirstDayOfWeek_inputLocaleWithoutFwTag_resultIsSun() throws Exception {
+ String result = LocalePreferences.getFirstDayOfWeek(Locale.forLanguageTag("en-US"));
+
+ assertEquals(LocalePreferences.FirstDayOfWeek.SUNDAY, result);
+ }
+
+ @Test
+ public void getFirstDayOfWeek_inputLocaleWithoutFwTagAndDisableResolved_resultIsEmpty()
+ throws Exception {
+ String result = LocalePreferences
+ .getFirstDayOfWeek(Locale.forLanguageTag("en-US"), false);
+
+ assertEquals(LocalePreferences.FirstDayOfWeek.DEFAULT, result);
+ }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/util/TypedValueCompatTest.kt b/core/core/src/androidTest/java/androidx/core/util/TypedValueCompatTest.kt
new file mode 100644
index 0000000..8ad6646
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/util/TypedValueCompatTest.kt
@@ -0,0 +1,166 @@
+/*
+ * 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.core.util
+
+import android.util.DisplayMetrics
+import android.util.TypedValue
+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 com.google.common.truth.Truth.assertWithMessage
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class TypedValueCompatTest {
+ @Test
+ fun invalidUnitThrows() {
+ val metrics: DisplayMetrics = mock(DisplayMetrics::class.java)
+ val fontScale = 2f
+ metrics.density = 1f
+ metrics.xdpi = 2f
+ metrics.scaledDensity = fontScale * metrics.density
+
+ assertThrows(IllegalArgumentException::class.java) {
+ TypedValueCompat.deriveDimension(TypedValue.COMPLEX_UNIT_MM + 1, 23f, metrics)
+ }
+ }
+
+ @Test
+ fun density0_deriveDoesNotCrash() {
+ val metrics: DisplayMetrics = mock(DisplayMetrics::class.java)
+ metrics.density = 0f
+ metrics.xdpi = 0f
+ metrics.scaledDensity = 0f
+
+ listOf(
+ TypedValue.COMPLEX_UNIT_DIP,
+ TypedValue.COMPLEX_UNIT_SP,
+ TypedValue.COMPLEX_UNIT_PT,
+ TypedValue.COMPLEX_UNIT_IN,
+ TypedValue.COMPLEX_UNIT_MM
+ )
+ .forEach { dimenType ->
+ assertThat(TypedValueCompat.deriveDimension(dimenType, 23f, metrics))
+ .isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun scaledDensity0_deriveSpDoesNotCrash() {
+ val metrics: DisplayMetrics = mock(DisplayMetrics::class.java)
+ metrics.density = 1f
+ metrics.xdpi = 2f
+ metrics.scaledDensity = 0f
+
+ assertThat(TypedValueCompat.deriveDimension(TypedValue.COMPLEX_UNIT_SP, 23f, metrics))
+ .isEqualTo(0)
+ }
+
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun deriveDimensionMatchesRealVersion() {
+ val metrics: DisplayMetrics = mock(DisplayMetrics::class.java)
+ metrics.density = 1f
+ metrics.xdpi = 2f
+ metrics.scaledDensity = 2f
+
+ listOf(
+ TypedValue.COMPLEX_UNIT_PX,
+ TypedValue.COMPLEX_UNIT_DIP,
+ TypedValue.COMPLEX_UNIT_SP,
+ TypedValue.COMPLEX_UNIT_PT,
+ TypedValue.COMPLEX_UNIT_IN,
+ TypedValue.COMPLEX_UNIT_MM
+ )
+ .forEach { dimenType ->
+ for (i: Int in -1000 until 1000) {
+ assertThat(TypedValueCompat.deriveDimension(dimenType, i.toFloat(), metrics))
+ .isWithin(0.05f)
+ .of(TypedValue.deriveDimension(dimenType, i.toFloat(), metrics))
+ }
+ }
+ }
+
+ @Test
+ fun eachUnitType_roundTripIsEqual() {
+ val metrics: DisplayMetrics = mock(DisplayMetrics::class.java)
+ metrics.density = 1f
+ metrics.xdpi = 2f
+ metrics.scaledDensity = 2f
+
+ listOf(
+ TypedValue.COMPLEX_UNIT_PX,
+ TypedValue.COMPLEX_UNIT_DIP,
+ TypedValue.COMPLEX_UNIT_SP,
+ TypedValue.COMPLEX_UNIT_PT,
+ TypedValue.COMPLEX_UNIT_IN,
+ TypedValue.COMPLEX_UNIT_MM
+ )
+ .forEach { dimenType ->
+ for (i: Int in -10000 until 10000) {
+ assertRoundTripIsEqual(i.toFloat(), dimenType, metrics)
+ assertRoundTripIsEqual(i - .1f, dimenType, metrics)
+ assertRoundTripIsEqual(i + .5f, dimenType, metrics)
+ }
+ }
+ }
+
+ @Test
+ fun convenienceFunctionsCallCorrectAliases() {
+ val metrics: DisplayMetrics = mock(DisplayMetrics::class.java)
+ metrics.density = 1f
+ metrics.xdpi = 2f
+ metrics.scaledDensity = 2f
+
+ assertThat(TypedValueCompat.pxToDp(20f, metrics))
+ .isWithin(0.05f)
+ .of(TypedValueCompat.deriveDimension(TypedValue.COMPLEX_UNIT_DIP, 20f, metrics))
+ assertThat(TypedValueCompat.pxToSp(20f, metrics))
+ .isWithin(0.05f)
+ .of(TypedValueCompat.deriveDimension(TypedValue.COMPLEX_UNIT_SP, 20f, metrics))
+ assertThat(TypedValueCompat.dpToPx(20f, metrics))
+ .isWithin(0.05f)
+ .of(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20f, metrics))
+ assertThat(TypedValueCompat.spToPx(20f, metrics))
+ .isWithin(0.05f)
+ .of(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20f, metrics))
+ }
+
+ private fun assertRoundTripIsEqual(
+ dimenValueToTest: Float,
+ dimenType: Int,
+ metrics: DisplayMetrics,
+ ) {
+ val actualPx = TypedValue.applyDimension(dimenType, dimenValueToTest, metrics)
+ val actualDimenValue = TypedValueCompat.deriveDimension(dimenType, actualPx, metrics)
+ assertWithMessage(
+ "TypedValue.applyDimension for type %s on %s = %s should equal " +
+ "TypedValueCompat.deriveDimension of %s",
+ dimenType,
+ dimenValueToTest,
+ actualPx,
+ actualDimenValue
+ )
+ .that(dimenValueToTest)
+ .isWithin(0.05f)
+ .of(actualDimenValue)
+ }
+}
\ No newline at end of file
diff --git a/core/core/src/androidTest/java/androidx/core/view/VelocityTrackerCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/VelocityTrackerCompatTest.java
new file mode 100644
index 0000000..bb8d29a
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/view/VelocityTrackerCompatTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.core.view;
+
+import static android.view.MotionEvent.AXIS_BRAKE;
+import static android.view.MotionEvent.AXIS_X;
+import static android.view.MotionEvent.AXIS_Y;
+
+import static androidx.core.view.MotionEventCompat.AXIS_SCROLL;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Build;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class VelocityTrackerCompatTest {
+ /** Arbitrarily chosen velocities across different supported dimensions and some pointer IDs. */
+ private static final float X_VEL_POINTER_ID_1 = 5;
+ private static final float X_VEL_POINTER_ID_2 = 6;
+ private static final float Y_VEL_POINTER_ID_1 = 7;
+ private static final float Y_VEL_POINTER_ID_2 = 8;
+ private static final float SCROLL_VEL_POINTER_ID_1 = 9;
+ private static final float SCROLL_VEL_POINTER_ID_2 = 10;
+
+ /**
+ * A small enough step time stamp (ms), that the VelocityTracker wouldn't consider big enough to
+ * assume a pointer has stopped.
+ */
+ private static final long TIME_STEP_MS = 10;
+
+ /**
+ * An arbitrarily chosen value for the number of times a movement particular type of movement
+ * is added to a tracker. For velocities to be non-zero, we should generally have 2/3 movements,
+ * so 4 is a good value to use.
+ */
+ private static final int NUM_MOVEMENTS = 4;
+
+ private VelocityTracker mPlanarTracker;
+ private VelocityTracker mScrollTracker;
+
+ @Before
+ public void setup() {
+ mPlanarTracker = VelocityTracker.obtain();
+ mScrollTracker = VelocityTracker.obtain();
+
+ long time = 0;
+ float xPointer1 = 0;
+ float yPointer1 = 0;
+ float scrollPointer1 = 0;
+ float xPointer2 = 0;
+ float yPointer2 = 0;
+ float scrollPointer2 = 0;
+
+ // Add MotionEvents to create some velocity!
+ // Note that: the goal of these tests is not to check the specific values of the velocities,
+ // but instead, compare the outputs of the Compat tracker against the platform tracker.
+ for (int i = 0; i < NUM_MOVEMENTS; i++) {
+ time += TIME_STEP_MS;
+ xPointer1 += X_VEL_POINTER_ID_1 * TIME_STEP_MS;
+ yPointer1 += Y_VEL_POINTER_ID_1 * TIME_STEP_MS;
+ scrollPointer1 = SCROLL_VEL_POINTER_ID_1 * TIME_STEP_MS;
+
+ xPointer2 += X_VEL_POINTER_ID_2 * TIME_STEP_MS;
+ yPointer2 += Y_VEL_POINTER_ID_2 * TIME_STEP_MS;
+ scrollPointer2 = SCROLL_VEL_POINTER_ID_2 * TIME_STEP_MS;
+
+ addPlanarMotionEvent(1, time, xPointer1, yPointer1);
+ addPlanarMotionEvent(2, time, xPointer2, yPointer2);
+ addScrollMotionEvent(1, time, scrollPointer1);
+ addScrollMotionEvent(2, time, scrollPointer2);
+ }
+
+ mPlanarTracker.computeCurrentVelocity(1000);
+ mScrollTracker.computeCurrentVelocity(1000);
+ }
+
+ @Test
+ public void testIsAxisSupported_planarAxes() {
+ assertTrue(VelocityTrackerCompat.isAxisSupported(VelocityTracker.obtain(), AXIS_X));
+ assertTrue(VelocityTrackerCompat.isAxisSupported(VelocityTracker.obtain(), AXIS_Y));
+ }
+
+ @Test
+ public void testIsAxisSupported_nonPlanarAxes() {
+ if (Build.VERSION.SDK_INT >= 34) {
+ assertTrue(
+ VelocityTrackerCompat.isAxisSupported(VelocityTracker.obtain(), AXIS_SCROLL));
+ } else {
+ assertFalse(
+ VelocityTrackerCompat.isAxisSupported(VelocityTracker.obtain(), AXIS_SCROLL));
+ }
+
+ // Check against an axis that has not yet been supported at any Android version.
+ assertFalse(VelocityTrackerCompat.isAxisSupported(VelocityTracker.obtain(), AXIS_BRAKE));
+ }
+
+ @Test
+ public void testGetAxisVelocity_planarAxes_noPointerId_againstEquivalentPlatformApis() {
+ if (Build.VERSION.SDK_INT >= 34) {
+ float compatXVelocity = VelocityTrackerCompat.getAxisVelocity(mPlanarTracker, AXIS_X);
+ float compatYVelocity = VelocityTrackerCompat.getAxisVelocity(mPlanarTracker, AXIS_Y);
+
+ assertEquals(mPlanarTracker.getAxisVelocity(AXIS_X), compatXVelocity, 0);
+ assertEquals(mPlanarTracker.getAxisVelocity(AXIS_Y), compatYVelocity, 0);
+ }
+ }
+
+ @Test
+ public void testGetAxisVelocity_planarAxes_withPointerId_againstEquivalentPlatformApis() {
+ if (Build.VERSION.SDK_INT >= 34) {
+ float compatXVelocity =
+ VelocityTrackerCompat.getAxisVelocity(mPlanarTracker, AXIS_X, 2);
+ float compatYVelocity =
+ VelocityTrackerCompat.getAxisVelocity(mPlanarTracker, AXIS_Y, 2);
+
+ assertEquals(mPlanarTracker.getAxisVelocity(AXIS_X, 2), compatXVelocity, 0);
+ assertEquals(mPlanarTracker.getAxisVelocity(AXIS_Y, 2), compatYVelocity, 0);
+ }
+ }
+
+ @Test
+ public void testGetAxisVelocity_planarAxes_noPointerId_againstGenericXAndYVelocityApis() {
+ float compatXVelocity = VelocityTrackerCompat.getAxisVelocity(mPlanarTracker, AXIS_X);
+ float compatYVelocity = VelocityTrackerCompat.getAxisVelocity(mPlanarTracker, AXIS_Y);
+
+ assertEquals(mPlanarTracker.getXVelocity(), compatXVelocity, 0);
+ assertEquals(mPlanarTracker.getYVelocity(), compatYVelocity, 0);
+ }
+
+ @Test
+ public void testGetAxisVelocity_planarAxes_withPointerId_againstGenericXAndYVelocityApis() {
+ float compatXVelocity =
+ VelocityTrackerCompat.getAxisVelocity(mPlanarTracker, AXIS_X, 2);
+ float compatYVelocity =
+ VelocityTrackerCompat.getAxisVelocity(mPlanarTracker, AXIS_Y, 2);
+
+ assertEquals(mPlanarTracker.getXVelocity(2), compatXVelocity, 0);
+ assertEquals(mPlanarTracker.getYVelocity(2), compatYVelocity, 0);
+ }
+
+ @Test
+ public void testGetAxisVelocity_axisScroll_noPointerId() {
+ float compatScrollVelocity =
+ VelocityTrackerCompat.getAxisVelocity(mScrollTracker, AXIS_SCROLL);
+
+ if (Build.VERSION.SDK_INT >= 34) {
+ assertEquals(mScrollTracker.getAxisVelocity(AXIS_SCROLL), compatScrollVelocity, 0);
+ } else {
+ assertEquals(0, compatScrollVelocity, 0);
+ }
+ }
+
+ @Test
+ public void testGetAxisVelocity_axisScroll_withPointerId() {
+ float compatScrollVelocity =
+ VelocityTrackerCompat.getAxisVelocity(mScrollTracker, AXIS_SCROLL, 2);
+
+ if (Build.VERSION.SDK_INT >= 34) {
+ assertEquals(mScrollTracker.getAxisVelocity(AXIS_SCROLL, 2), compatScrollVelocity, 0);
+ } else {
+ assertEquals(0, compatScrollVelocity, 0);
+ }
+ }
+
+
+ private void addPlanarMotionEvent(int pointerId, long time, float x, float y) {
+ MotionEvent ev = MotionEvent.obtain(0L, time, MotionEvent.ACTION_MOVE, x, y, 0);
+ mPlanarTracker.addMovement(ev);
+ ev.recycle();
+ }
+ private void addScrollMotionEvent(int pointerId, long time, float scrollAmount) {
+ MotionEvent.PointerProperties props = new MotionEvent.PointerProperties();
+ props.id = pointerId;
+
+ MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+ coords.setAxisValue(MotionEvent.AXIS_SCROLL, scrollAmount);
+
+ MotionEvent ev = MotionEvent.obtain(0 /* downTime */,
+ time,
+ MotionEvent.ACTION_SCROLL,
+ 1 /* pointerCount */,
+ new MotionEvent.PointerProperties[] {props},
+ new MotionEvent.PointerCoords[] {coords},
+ 0 /* metaState */,
+ 0 /* buttonState */,
+ 0 /* xPrecision */,
+ 0 /* yPrecision */,
+ 1 /* deviceId */,
+ 0 /* edgeFlags */,
+ InputDevice.SOURCE_ROTARY_ENCODER,
+ 0 /* flags */);
+ mScrollTracker.addMovement(ev);
+ ev.recycle();
+ }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
index 805a399..f08eeed 100644
--- a/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
@@ -325,4 +325,16 @@
accessibilityNodeInfoCompat.setTextSelectable(true);
assertThat(accessibilityNodeInfoCompat.isTextSelectable(), equalTo(true));
}
+
+ @SdkSuppress(minSdkVersion = 34)
+ @SmallTest
+ @Test
+ public void testActionScrollInDirection() {
+ AccessibilityActionCompat actionCompat =
+ AccessibilityActionCompat.ACTION_SCROLL_IN_DIRECTION;
+ assertThat(actionCompat.getId(),
+ is(getExpectedActionId(android.R.id.accessibilityActionScrollInDirection)));
+ assertThat(actionCompat.toString(), is("AccessibilityActionCompat: "
+ + "ACTION_SCROLL_IN_DIRECTION"));
+ }
}
diff --git a/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompatTest.java
index a1afdfda..1788e22 100644
--- a/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompatTest.java
@@ -17,7 +17,9 @@
package androidx.core.view.accessibility;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsEqual.equalTo;
+import static org.hamcrest.core.IsNot.not;
import android.annotation.TargetApi;
import android.graphics.Region;
@@ -40,6 +42,17 @@
return AccessibilityWindowInfoCompat.wrapNonNullInstance(accessibilityWindowInfo);
}
+ @SdkSuppress(minSdkVersion = 30)
+ @SmallTest
+ @Test
+ public void testConstructor() {
+ AccessibilityWindowInfoCompat infoCompat = new AccessibilityWindowInfoCompat();
+ AccessibilityWindowInfo info = new AccessibilityWindowInfo();
+
+ assertThat(infoCompat.unwrap(), is(not(equalTo(null))));
+ assertThat(infoCompat.unwrap(), equalTo(info));
+ }
+
@SdkSuppress(minSdkVersion = 33)
@SmallTest
@Test
diff --git a/core/core/src/main/java/androidx/core/app/GrammaticalInflectionManagerCompat.java b/core/core/src/main/java/androidx/core/app/GrammaticalInflectionManagerCompat.java
new file mode 100644
index 0000000..d1f442b
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/app/GrammaticalInflectionManagerCompat.java
@@ -0,0 +1,137 @@
+/*
+ * 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.core.app;
+
+import android.app.GrammaticalInflectionManager;
+import android.content.Context;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.DoNotInline;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.core.os.BuildCompat;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Helper for accessing features in {@link android.app.GrammaticalInflectionManager}.
+ */
+public final class GrammaticalInflectionManagerCompat {
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @IntDef(value = {
+ GRAMMATICAL_GENDER_NOT_SPECIFIED,
+ GRAMMATICAL_GENDER_NEUTRAL,
+ GRAMMATICAL_GENDER_FEMININE,
+ GRAMMATICAL_GENDER_MASCULINE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface GrammaticalGender {}
+
+ /**
+ * Constant for grammatical gender: to indicate the user has not specified the terms
+ * of address for the application.
+ *
+ * @see android.content.res.Configuration#GRAMMATICAL_GENDER_NOT_SPECIFIED
+ */
+ public static final int GRAMMATICAL_GENDER_NOT_SPECIFIED = 0;
+
+ /**
+ * Constant for grammatical gender: to indicate the terms of address the user
+ * preferred in an application is neuter.
+ *
+ * @see android.content.res.Configuration#GRAMMATICAL_GENDER_NEUTRAL
+ */
+ public static final int GRAMMATICAL_GENDER_NEUTRAL = 1;
+
+ /**
+ * Constant for grammatical gender: to indicate the terms of address the user
+ * preferred in an application is feminine.
+ *
+ * @see android.content.res.Configuration#GRAMMATICAL_GENDER_FEMININE
+ */
+ public static final int GRAMMATICAL_GENDER_FEMININE = 2;
+
+ /**
+ * Constant for grammatical gender: to indicate the terms of address the user
+ * preferred in an application is masculine.
+ *
+ * @see android.content.res.Configuration#GRAMMATICAL_GENDER_MASCULINE
+ */
+ public static final int GRAMMATICAL_GENDER_MASCULINE = 3;
+
+ private GrammaticalInflectionManagerCompat() {}
+
+ /**
+ * Returns the current grammatical gender. No-op on versions prior to
+ * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}.
+ *
+ * @param context Context to retrieve service from.
+ * @return the grammatical gender if device API level is greater than 33, otherwise, return 0.
+ */
+ @OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
+ @AnyThread
+ public static int getApplicationGrammaticalGender(@NonNull Context context) {
+ if (BuildCompat.isAtLeastU()) {
+ return Api34Impl.getApplicationGrammaticalGender(context);
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Sets the current grammatical gender. No-op on versions prior to
+ * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}.
+ *
+ * @param context Context to retrieve service from.
+ * @param grammaticalGender the terms of address the user preferred in an application.
+ */
+ @OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
+ @AnyThread
+ public static void setRequestedApplicationGrammaticalGender(
+ @NonNull Context context, @GrammaticalGender int grammaticalGender) {
+ if (BuildCompat.isAtLeastU()) {
+ Api34Impl.setRequestedApplicationGrammaticalGender(context, grammaticalGender);
+ }
+ }
+
+ @RequiresApi(34)
+ static class Api34Impl {
+ private Api34Impl() {}
+
+ @DoNotInline
+ static int getApplicationGrammaticalGender(Context context) {
+ return getGrammaticalInflectionManager(context).getApplicationGrammaticalGender();
+ }
+
+ @DoNotInline
+ static void setRequestedApplicationGrammaticalGender(
+ Context context, int grammaticalGender) {
+ getGrammaticalInflectionManager(context)
+ .setRequestedApplicationGrammaticalGender(grammaticalGender);
+ }
+
+ private static GrammaticalInflectionManager getGrammaticalInflectionManager(
+ Context context) {
+ return context.getSystemService(GrammaticalInflectionManager.class);
+ }
+ }
+}
diff --git a/core/core/src/main/java/androidx/core/app/NotificationManagerCompat.java b/core/core/src/main/java/androidx/core/app/NotificationManagerCompat.java
index ae70ed1..48bf924 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationManagerCompat.java
@@ -24,12 +24,14 @@
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
+import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.os.Build;
@@ -808,6 +810,41 @@
}
/**
+ * Returns whether the calling app can send fullscreen intents.
+ *
+ * <p>Fullscreen intents were introduced in Android
+ * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, where apps could always attach a full
+ * screen intent to their notification via
+ * {@link Notification.Builder#setFullScreenIntent(PendingIntent, boolean)}}.
+ *
+ * <p>Android {@link android.os.Build.VERSION_CODES#Q} introduced the
+ * {@link android.Manifest.permission#USE_FULL_SCREEN_INTENT}
+ * permission, where SystemUI will only show the full screen intent attached to a notification
+ * if the permission is declared in the manifest.
+ *
+ * <p>Starting from Android {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, apps
+ * may not have permission to use {@link android.Manifest.permission#USE_FULL_SCREEN_INTENT}. If
+ * the FSI permission is denied, SystemUI will show the notification as an expanded heads up
+ * notification on lockscreen.
+ *
+ * <p>To request access, add the {@link android.Manifest.permission#USE_FULL_SCREEN_INTENT}
+ * permission to your manifest, and use
+ * {@link android.provider.Settings#ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT} to send the user
+ * to the settings page where they can grant your app the FSI permission.
+ */
+ public boolean canUseFullScreenIntent() {
+ if (Build.VERSION.SDK_INT < 29) {
+ return true;
+ }
+ if (Build.VERSION.SDK_INT < 34) {
+ final int permissionState =
+ mContext.checkSelfPermission(Manifest.permission.USE_FULL_SCREEN_INTENT);
+ return permissionState == PackageManager.PERMISSION_GRANTED;
+ }
+ return Api34Impl.canUseFullScreenIntent(mNotificationManager);
+ }
+
+ /**
* Returns true if this notification should use the side channel for delivery.
*/
private static boolean useSideChannelForNotification(Notification notification) {
@@ -1362,4 +1399,18 @@
}
}
+ /**
+ * A class for wrapping calls to {@link Notification.Builder} methods which
+ * were added in API 34; these calls must be wrapped to avoid performance issues.
+ * See the UnsafeNewApiCall lint rule for more details.
+ */
+ @RequiresApi(34)
+ static class Api34Impl {
+ private Api34Impl() { }
+
+ @DoNotInline
+ static boolean canUseFullScreenIntent(NotificationManager notificationManager) {
+ return notificationManager.canUseFullScreenIntent();
+ }
+ }
}
diff --git a/core/core/src/main/java/androidx/core/app/ServiceCompat.java b/core/core/src/main/java/androidx/core/app/ServiceCompat.java
index 69d1a20..09d7b64 100644
--- a/core/core/src/main/java/androidx/core/app/ServiceCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ServiceCompat.java
@@ -16,17 +16,23 @@
package androidx.core.app;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE;
+
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import android.app.Notification;
import android.app.Service;
+import android.content.pm.ServiceInfo;
import android.os.Build;
import androidx.annotation.DoNotInline;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import androidx.core.os.BuildCompat;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -88,6 +94,92 @@
@Retention(RetentionPolicy.SOURCE)
public @interface StopForegroundFlags {}
+ private static final int FOREGROUND_SERVICE_TYPE_ALLOWED_SINCE_Q =
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+
+ private static final int FOREGROUND_SERVICE_TYPE_ALLOWED_SINCE_U =
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE;
+
+ /**
+ * {@link Service#startForeground(int, Notification, int)} with the third parameter
+ * {@code foregroundServiceType} was added in {@link android.os.Build.VERSION_CODES#Q}.
+ *
+ * <p>Before SDK Version {@link android.os.Build.VERSION_CODES#Q}, this method call should call
+ * {@link Service#startForeground(int, Notification)} without the {@code foregroundServiceType}
+ * parameter.</p>
+ *
+ * <p>Beginning with SDK Version {@link android.os.Build.VERSION_CODES#Q}, the allowed
+ * foregroundServiceType are:
+ * <ul>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MANIFEST}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_NONE}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_PHONE_CALL}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_LOCATION}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_CAMERA}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MICROPHONE}</li>
+ * </ul>
+ * </p>
+ *
+ * <p>Beginning with SDK Version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * apps targeting SDK Version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} is not
+ * allowed to use {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_NONE}. The allowed
+ * foregroundServiceType are:
+ * <ul>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MANIFEST}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_PHONE_CALL}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_LOCATION}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_CAMERA}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MICROPHONE}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_HEALTH}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SHORT_SERVICE}</li>
+ * <li>{@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE}</li>
+ * </ul>
+ * </p>
+ *
+ * @see Service#startForeground(int, Notification)
+ * @see Service#startForeground(int, Notification, int)
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ public static void startForeground(@NonNull Service service, int id,
+ @NonNull Notification notification, int foregroundServiceType) {
+ if (BuildCompat.isAtLeastU()) {
+ Api34Impl.startForeground(service, id, notification, foregroundServiceType);
+ } else if (Build.VERSION.SDK_INT >= 29) {
+ Api29Impl.startForeground(service, id, notification, foregroundServiceType);
+ } else {
+ service.startForeground(id, notification);
+ }
+ }
+
/**
* Remove the passed service from foreground state, allowing it to be killed if
* more memory is needed.
@@ -115,4 +207,43 @@
service.stopForeground(flags);
}
}
+
+ @RequiresApi(29)
+ static class Api29Impl {
+ private Api29Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static void startForeground(Service service, int id, Notification notification,
+ int foregroundServiceType) {
+ if (foregroundServiceType == FOREGROUND_SERVICE_TYPE_NONE
+ || foregroundServiceType == FOREGROUND_SERVICE_TYPE_MANIFEST) {
+ service.startForeground(id, notification, foregroundServiceType);
+ } else {
+ service.startForeground(id, notification,
+ foregroundServiceType & FOREGROUND_SERVICE_TYPE_ALLOWED_SINCE_Q);
+ }
+ }
+ }
+
+ @RequiresApi(34)
+ static class Api34Impl {
+ private Api34Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static void startForeground(Service service, int id, Notification notification,
+ int foregroundServiceType) {
+ if (foregroundServiceType == FOREGROUND_SERVICE_TYPE_NONE
+ || foregroundServiceType == FOREGROUND_SERVICE_TYPE_MANIFEST) {
+ service.startForeground(id, notification, foregroundServiceType);
+ } else {
+ service.startForeground(id, notification,
+ foregroundServiceType & FOREGROUND_SERVICE_TYPE_ALLOWED_SINCE_U);
+ }
+ }
+ }
+
}
diff --git a/core/core/src/main/java/androidx/core/location/LocationCompat.java b/core/core/src/main/java/androidx/core/location/LocationCompat.java
index 6ccefa8..2e2c045 100644
--- a/core/core/src/main/java/androidx/core/location/LocationCompat.java
+++ b/core/core/src/main/java/androidx/core/location/LocationCompat.java
@@ -81,7 +81,8 @@
@Nullable
private static Method sSetIsFromMockProviderMethod;
- private LocationCompat() {}
+ private LocationCompat() {
+ }
/**
* Return the time of this fix, in nanoseconds of elapsed real-time since system boot.
@@ -295,9 +296,17 @@
/**
* Returns the Mean Sea Level altitude of the location in meters.
*
+ * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude does not exist. In
+ * order to allow for backwards compatibility and testing however, this method will attempt
+ * to read a double extra with the key {@link #EXTRA_MSL_ALTITUDE} and return the result.
+ *
* @throws IllegalStateException if the Mean Sea Level altitude of the location is not set
+ * @see Location#getMslAltitudeMeters()
*/
public static double getMslAltitudeMeters(@NonNull Location location) {
+ if (VERSION.SDK_INT >= 34) {
+ return Api34Impl.getMslAltitudeMeters(location);
+ }
Preconditions.checkState(hasMslAltitude(location),
"The Mean Sea Level altitude of the location is not set.");
return getOrCreateExtras(location).getDouble(EXTRA_MSL_ALTITUDE);
@@ -305,24 +314,54 @@
/**
* Sets the Mean Sea Level altitude of the location in meters.
+ *
+ * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude does not exist. In
+ * order to allow for backwards compatibility and testing however, this method will attempt
+ * to set a double extra with the key {@link #EXTRA_MSL_ALTITUDE} to include Mean Sea Level
+ * altitude. Be aware that this will overwrite any prior extra value under the same key.
+ *
+ * @see Location#setMslAltitudeMeters(double)
*/
public static void setMslAltitudeMeters(@NonNull Location location,
double mslAltitudeMeters) {
- getOrCreateExtras(location).putDouble(EXTRA_MSL_ALTITUDE, mslAltitudeMeters);
+ if (VERSION.SDK_INT >= 34) {
+ Api34Impl.setMslAltitudeMeters(location, mslAltitudeMeters);
+ } else {
+ getOrCreateExtras(location).putDouble(EXTRA_MSL_ALTITUDE, mslAltitudeMeters);
+ }
}
/**
* Returns true if the location has a Mean Sea Level altitude, false otherwise.
+ *
+ * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude does not exist. In
+ * order to allow for backwards compatibility and testing however, this method will return
+ * true if an extra value is with the key {@link #EXTRA_MSL_ALTITUDE}.
+ *
+ * @see Location#hasMslAltitude()
*/
public static boolean hasMslAltitude(@NonNull Location location) {
+ if (VERSION.SDK_INT >= 34) {
+ return Api34Impl.hasMslAltitude(location);
+ }
return containsExtra(location, EXTRA_MSL_ALTITUDE);
}
/**
* Removes the Mean Sea Level altitude from the location.
+ *
+ * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude does not exist. In
+ * order to allow for backwards compatibility and testing however, this method will attempt
+ * to remove any extra value with the key {@link #EXTRA_MSL_ALTITUDE}.
+ *
+ * @see Location#removeMslAltitude()
*/
public static void removeMslAltitude(@NonNull Location location) {
- removeExtra(location, EXTRA_MSL_ALTITUDE);
+ if (VERSION.SDK_INT >= 34) {
+ Api34Impl.removeMslAltitude(location);
+ } else {
+ removeExtra(location, EXTRA_MSL_ALTITUDE);
+ }
}
/**
@@ -331,11 +370,20 @@
* altitude of the location falls within {@link #getMslAltitudeMeters(Location)} +/- this
* uncertainty.
*
+ * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude accuracy does not
+ * exist. In order to allow for backwards compatibility and testing however, this method will
+ * attempt to read a float extra with the key {@link #EXTRA_MSL_ALTITUDE_ACCURACY} and return
+ * the result.
+ *
* @throws IllegalStateException if the Mean Sea Level altitude accuracy of the location is not
* set
+ * @see Location#setMslAltitudeAccuracyMeters(float)
*/
public static @FloatRange(from = 0.0) float getMslAltitudeAccuracyMeters(
@NonNull Location location) {
+ if (VERSION.SDK_INT >= 34) {
+ return Api34Impl.getMslAltitudeAccuracyMeters(location);
+ }
Preconditions.checkState(hasMslAltitudeAccuracy(location),
"The Mean Sea Level altitude accuracy of the location is not set.");
return getOrCreateExtras(location).getFloat(EXTRA_MSL_ALTITUDE_ACCURACY);
@@ -343,25 +391,56 @@
/**
* Sets the Mean Sea Level altitude accuracy of the location in meters.
+ *
+ * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude accuracy does not
+ * exist. In order to allow for backwards compatibility and testing however, this method will
+ * attempt to set a float extra with the key {@link #EXTRA_MSL_ALTITUDE_ACCURACY} to include
+ * Mean Sea Level altitude accuracy. Be aware that this will overwrite any prior extra value
+ * under the same key.
+ *
+ * @see Location#setMslAltitudeAccuracyMeters(float)
*/
public static void setMslAltitudeAccuracyMeters(@NonNull Location location,
@FloatRange(from = 0.0) float mslAltitudeAccuracyMeters) {
- getOrCreateExtras(location).putFloat(EXTRA_MSL_ALTITUDE_ACCURACY,
- mslAltitudeAccuracyMeters);
+ if (VERSION.SDK_INT >= 34) {
+ Api34Impl.setMslAltitudeAccuracyMeters(location, mslAltitudeAccuracyMeters);
+ } else {
+ getOrCreateExtras(location).putFloat(EXTRA_MSL_ALTITUDE_ACCURACY,
+ mslAltitudeAccuracyMeters);
+ }
}
/**
* Returns true if the location has a Mean Sea Level altitude accuracy, false otherwise.
+ *
+ * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude accuracy does not
+ * exist. In order to allow for backwards compatibility and testing however, this method will
+ * return true if an extra value is with the key {@link #EXTRA_MSL_ALTITUDE_ACCURACY}.
+ *
+ * @see Location#hasMslAltitudeAccuracy()
*/
public static boolean hasMslAltitudeAccuracy(@NonNull Location location) {
+ if (VERSION.SDK_INT >= 34) {
+ return Api34Impl.hasMslAltitudeAccuracy(location);
+ }
return containsExtra(location, EXTRA_MSL_ALTITUDE_ACCURACY);
}
/**
* Removes the Mean Sea Level altitude accuracy from the location.
+ *
+ * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude accuracy does not
+ * exist. In order to allow for backwards compatibility and testing however, this method will
+ * attempt to remove any extra value with the key {@link #EXTRA_MSL_ALTITUDE_ACCURACY}.
+ *
+ * @see Location#removeMslAltitudeAccuracy()
*/
public static void removeMslAltitudeAccuracy(@NonNull Location location) {
- removeExtra(location, EXTRA_MSL_ALTITUDE_ACCURACY);
+ if (VERSION.SDK_INT >= 34) {
+ Api34Impl.removeMslAltitudeAccuracy(location);
+ } else {
+ removeExtra(location, EXTRA_MSL_ALTITUDE_ACCURACY);
+ }
}
/**
@@ -433,10 +512,59 @@
}
}
+ @RequiresApi(34)
+ private static class Api34Impl {
+
+ private Api34Impl() {
+ }
+
+ @DoNotInline
+ static double getMslAltitudeMeters(Location location) {
+ return location.getMslAltitudeMeters();
+ }
+
+ @DoNotInline
+ static void setMslAltitudeMeters(Location location, double mslAltitudeMeters) {
+ location.setMslAltitudeMeters(mslAltitudeMeters);
+ }
+
+ @DoNotInline
+ static boolean hasMslAltitude(Location location) {
+ return location.hasMslAltitude();
+ }
+
+ @DoNotInline
+ static void removeMslAltitude(Location location) {
+ location.removeMslAltitude();
+ }
+
+ @DoNotInline
+ static float getMslAltitudeAccuracyMeters(Location location) {
+ return location.getMslAltitudeAccuracyMeters();
+ }
+
+ @DoNotInline
+ static void setMslAltitudeAccuracyMeters(Location location,
+ float mslAltitudeAccuracyMeters) {
+ location.setMslAltitudeAccuracyMeters(mslAltitudeAccuracyMeters);
+ }
+
+ @DoNotInline
+ static boolean hasMslAltitudeAccuracy(Location location) {
+ return location.hasMslAltitudeAccuracy();
+ }
+
+ @DoNotInline
+ static void removeMslAltitudeAccuracy(Location location) {
+ location.removeMslAltitudeAccuracy();
+ }
+ }
+
@RequiresApi(26)
private static class Api26Impl {
- private Api26Impl() {}
+ private Api26Impl() {
+ }
@DoNotInline
static boolean hasVerticalAccuracy(Location location) {
@@ -487,7 +615,8 @@
@RequiresApi(18)
private static class Api18Impl {
- private Api18Impl() {}
+ private Api18Impl() {
+ }
@DoNotInline
static boolean isMock(Location location) {
@@ -498,7 +627,8 @@
@RequiresApi(17)
private static class Api17Impl {
- private Api17Impl() {}
+ private Api17Impl() {
+ }
@DoNotInline
static long getElapsedRealtimeNanos(Location location) {
diff --git a/core/core/src/main/java/androidx/core/service/quicksettings/PendingIntentActivityWrapper.java b/core/core/src/main/java/androidx/core/service/quicksettings/PendingIntentActivityWrapper.java
new file mode 100644
index 0000000..d42dd7c
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/service/quicksettings/PendingIntentActivityWrapper.java
@@ -0,0 +1,108 @@
+/*
+ * 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.core.service.quicksettings;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.service.quicksettings.TileService;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.PendingIntentCompat;
+
+/**
+ * A wrapper class for developers to use with
+ * {@link TileServiceCompat#startActivityAndCollapse(TileService, PendingIntentActivityWrapper)}.
+ */
+public class PendingIntentActivityWrapper {
+
+ private final Context mContext;
+
+ private final int mRequestCode;
+
+ @NonNull
+ private final Intent mIntent;
+
+ @PendingIntentCompat.Flags
+ private final int mFlags;
+
+ @Nullable
+ private final Bundle mOptions;
+
+ @Nullable
+ private final PendingIntent mPendingIntent;
+
+ private final boolean mIsMutable;
+
+ public PendingIntentActivityWrapper(@NonNull Context context, int requestCode,
+ @NonNull Intent intent,
+ @PendingIntentCompat.Flags int flags, boolean isMutable) {
+ this(context, requestCode, intent, flags, null, isMutable);
+ }
+
+ public PendingIntentActivityWrapper(@NonNull Context context, int requestCode,
+ @NonNull Intent intent,
+ @PendingIntentCompat.Flags int flags, @Nullable Bundle options, boolean isMutable) {
+ this.mContext = context;
+ this.mRequestCode = requestCode;
+ this.mIntent = intent;
+ this.mFlags = flags;
+ this.mOptions = options;
+ this.mIsMutable = isMutable;
+
+ mPendingIntent = createPendingIntent();
+ }
+
+ public @NonNull Context getContext() {
+ return mContext;
+ }
+
+ public int getRequestCode() {
+ return mRequestCode;
+ }
+
+ public @NonNull Intent getIntent() {
+ return mIntent;
+ }
+
+ public int getFlags() {
+ return mFlags;
+ }
+
+ public @NonNull Bundle getOptions() {
+ return mOptions;
+ }
+
+ public boolean isMutable() {
+ return mIsMutable;
+ }
+
+ public @Nullable PendingIntent getPendingIntent() {
+ return mPendingIntent;
+ }
+
+ private @Nullable PendingIntent createPendingIntent() {
+ if (mOptions == null) {
+ return PendingIntentCompat.getActivity(mContext, mRequestCode, mIntent, mFlags,
+ mIsMutable);
+ }
+ return PendingIntentCompat.getActivity(mContext, mRequestCode, mIntent, mFlags, mOptions,
+ mIsMutable);
+ }
+}
diff --git a/core/core/src/main/java/androidx/core/service/quicksettings/TileServiceCompat.java b/core/core/src/main/java/androidx/core/service/quicksettings/TileServiceCompat.java
new file mode 100644
index 0000000..cf1129f
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/service/quicksettings/TileServiceCompat.java
@@ -0,0 +1,96 @@
+/*
+ * 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.core.service.quicksettings;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.service.quicksettings.TileService;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * A helper for accessing {@link TileService} API methods.
+ */
+public class TileServiceCompat {
+
+ private static TileServiceWrapper sTileServiceWrapper;
+
+ /**
+ * Calls the correct {@link TileService}#startActivityAndCollapse() method
+ * depending on the app's targeted {@link android.os.Build.VERSION_CODES}.
+ */
+ public static void startActivityAndCollapse(@NonNull TileService tileService,
+ @NonNull PendingIntentActivityWrapper wrapper) {
+ if (SDK_INT >= 34) {
+ if (sTileServiceWrapper != null) {
+ sTileServiceWrapper.startActivityAndCollapse(wrapper.getPendingIntent());
+ } else {
+ Api34Impl.startActivityAndCollapse(tileService, wrapper.getPendingIntent());
+ }
+ } else if (SDK_INT >= 24) {
+ if (sTileServiceWrapper != null) {
+ sTileServiceWrapper.startActivityAndCollapse(wrapper.getIntent());
+ } else {
+ Api24Impl.startActivityAndCollapse(tileService, wrapper.getIntent());
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static void setTileServiceWrapper(@NonNull TileServiceWrapper serviceWrapper) {
+ sTileServiceWrapper = serviceWrapper;
+ }
+
+ /**
+ * @hide
+ */
+ public static void clearTileServiceWrapper() {
+ sTileServiceWrapper = null;
+ }
+
+ @RequiresApi(34)
+ private static class Api34Impl {
+ @DoNotInline
+ static void startActivityAndCollapse(TileService service,
+ PendingIntent pendingIntent) {
+ service.startActivityAndCollapse(pendingIntent);
+ }
+ }
+
+ @RequiresApi(24)
+ private static class Api24Impl {
+ @DoNotInline
+ static void startActivityAndCollapse(TileService service, Intent intent) {
+ service.startActivityAndCollapse(intent);
+ }
+ }
+
+ private TileServiceCompat() {
+ }
+
+ interface TileServiceWrapper {
+ void startActivityAndCollapse(PendingIntent pendingIntent);
+
+ void startActivityAndCollapse(Intent intent);
+ }
+}
diff --git a/core/core/src/main/java/androidx/core/text/util/LocalePreferences.java b/core/core/src/main/java/androidx/core/text/util/LocalePreferences.java
new file mode 100644
index 0000000..3db0031
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/text/util/LocalePreferences.java
@@ -0,0 +1,648 @@
+/*
+ * Copyright (C) 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.core.text.util;
+
+import android.icu.number.LocalizedNumberFormatter;
+import android.icu.number.NumberFormatter;
+import android.icu.text.DateFormat;
+import android.icu.text.DateTimePatternGenerator;
+import android.icu.util.MeasureUnit;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.StringDef;
+import androidx.core.os.BuildCompat;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Locale.Category;
+
+/**
+ * Provides friendly APIs to get the user's locale preferences. The data can refer to
+ * external/cldr/common/main/en.xml.
+ */
+@RequiresApi(VERSION_CODES.LOLLIPOP)
+public final class LocalePreferences {
+ private static final String TAG = LocalePreferences.class.getSimpleName();
+
+ /** APIs to get the user's preference of the hour cycle. */
+ public static class HourCycle {
+ private static final String U_EXTENSION_TAG = "hc";
+
+ /** 12 Hour System (0-11) */
+ public static final String H11 = "h11";
+ /** 12 Hour System (1-12) */
+ public static final String H12 = "h12";
+ /** 24 Hour System (0-23) */
+ public static final String H23 = "h23";
+ /** 24 Hour System (1-24) */
+ public static final String H24 = "h24";
+ /** Default hour cycle for the locale */
+ public static final String DEFAULT = "";
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @StringDef({
+ H11,
+ H12,
+ H23,
+ H24,
+ DEFAULT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface HourCycleTypes {
+ }
+
+ private HourCycle() {
+ }
+ }
+
+ /**
+ * Return the user's preference of the hour cycle which is from
+ * {@link Locale#getDefault(Locale.Category)}. The returned result is resolved and
+ * bases on the {@code Locale#getDefault(Locale.Category)}. It is one of the strings defined in
+ * {@see HourCycle}, e.g. {@code HourCycle#H11}.
+ */
+ @NonNull
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @HourCycle.HourCycleTypes
+ public static String getHourCycle() {
+ return getHourCycle(true);
+ }
+
+ /**
+ * Return the hour cycle setting of the inputted {@link Locale}. The returned result is resolved
+ * and based on the input {@code Locale}. It is one of the strings defined in
+ * {@see HourCycle}, e.g. {@code HourCycle#H11}.
+ */
+ @NonNull
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @HourCycle.HourCycleTypes
+ public static String getHourCycle(@NonNull Locale locale) {
+ return getHourCycle(locale, true);
+ }
+
+ /**
+ * Return the user's preference of the hour cycle which is from
+ * {@link Locale#getDefault(Locale.Category)}, e.g. {@code HourCycle#H11}.
+ *
+ * @param resolved If the {@code Locale#getDefault(Locale.Category)} contains hour cycle subtag,
+ * this argument is ignored. If the
+ * {@code Locale#getDefault(Locale.Category)} doesn't contain hour cycle subtag
+ * and the resolved argument is true, this function tries to find the default
+ * hour cycle for the {@code Locale#getDefault(Locale.Category)}. If the
+ * {@code Locale#getDefault(Locale.Category)} doesn't contain hour cycle subtag
+ * and the resolved argument is false, this function returns empty string
+ * , i.e. {@code HourCycle#DEFAULT}.
+ * @return {@link HourCycle.HourCycleTypes} If the malformed hour cycle format was specified
+ * in the hour cycle subtag, e.g. en-US-u-hc-h32, this function returns empty string, i.e.
+ * {@code HourCycle#DEFAULT}.
+ */
+ @NonNull
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @HourCycle.HourCycleTypes
+ public static String getHourCycle(
+ boolean resolved) {
+ Locale defaultLocale = (Build.VERSION.SDK_INT >= VERSION_CODES.N)
+ ? Api24Impl.getDefaultLocale()
+ : getDefaultLocale();
+ return getHourCycle(defaultLocale, resolved);
+ }
+
+ /**
+ * Return the hour cycle setting of the inputted {@link Locale}. E.g. "en-US-u-hc-h23".
+ *
+ * @param locale The {@code Locale} to get the hour cycle.
+ * @param resolved If the given {@code Locale} contains hour cycle subtag, this argument is
+ * ignored. If the given {@code Locale} doesn't contain hour cycle subtag and
+ * the resolved argument is true, this function tries to find the default
+ * hour cycle for the given {@code Locale}. If the given {@code Locale} doesn't
+ * contain hour cycle subtag and the resolved argument is false, this function
+ * return empty string, i.e. {@code HourCycle#DEFAULT}.
+ * @return {@link HourCycle.HourCycleTypes} If the malformed hour cycle format was specified
+ * in the hour cycle subtag, e.g. en-US-u-hc-h32, this function returns empty string, i.e.
+ * {@code HourCycle#DEFAULT}.
+ */
+ @NonNull
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @HourCycle.HourCycleTypes
+ public static String getHourCycle(@NonNull Locale locale, boolean resolved) {
+ String result = getUnicodeLocaleType(HourCycle.U_EXTENSION_TAG,
+ HourCycle.DEFAULT, locale, resolved);
+ if (result != null) {
+ return result;
+ }
+ if (BuildCompat.isAtLeastT()) {
+ return Api33Impl.getHourCycle(locale);
+ } else {
+ return getBaseHourCycle(locale);
+ }
+ }
+
+ /** APIs to get the user's preference of Calendar. */
+ public static class CalendarType {
+ private static final String U_EXTENSION_TAG = "ca";
+ /** Chinese Calendar */
+ public static final String CHINESE = "chinese";
+ /** Dangi Calendar (Korea Calendar) */
+ public static final String DANGI = "dangi";
+ /** Gregorian Calendar */
+ public static final String GREGORIAN = "gregorian";
+ /** Hebrew Calendar */
+ public static final String HEBREW = "hebrew";
+ /** Indian National Calendar */
+ public static final String INDIAN = "indian";
+ /** Islamic Calendar */
+ public static final String ISLAMIC = "islamic";
+ /** Islamic Calendar (tabular, civil epoch) */
+ public static final String ISLAMIC_CIVIL = "islamic-civil";
+ /** Islamic Calendar (Saudi Arabia, sighting) */
+ public static final String ISLAMIC_RGSA = "islamic-rgsa";
+ /** Islamic Calendar (tabular, astronomical epoch) */
+ public static final String ISLAMIC_TBLA = "islamic-tbla";
+ /** Islamic Calendar (Umm al-Qura) */
+ public static final String ISLAMIC_UMALQURA = "islamic-umalqura";
+ /** Persian Calendar */
+ public static final String PERSIAN = "persian";
+ /** Default calendar for the locale */
+ public static final String DEFAULT = "";
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @StringDef({
+ CHINESE,
+ DANGI,
+ GREGORIAN,
+ HEBREW,
+ INDIAN,
+ ISLAMIC,
+ ISLAMIC_CIVIL,
+ ISLAMIC_RGSA,
+ ISLAMIC_TBLA,
+ ISLAMIC_UMALQURA,
+ PERSIAN,
+ DEFAULT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CalendarTypes {
+ }
+
+ private CalendarType() {
+ }
+ }
+
+ /**
+ * Return the user's preference of the calendar type which is from {@link
+ * Locale#getDefault(Locale.Category)}. The returned result is resolved and bases on
+ * the {@code Locale#getDefault(Locale.Category)} settings. It is one of the strings defined in
+ * {@see CalendarType}, e.g. {@code CalendarType#CHINESE}.
+ */
+ @NonNull
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @CalendarType.CalendarTypes
+ public static String getCalendarType() {
+ return getCalendarType(true);
+ }
+
+ /**
+ * Return the calendar type of the inputted {@link Locale}. The returned result is resolved and
+ * based on the input {@link Locale} settings. It is one of the strings defined in
+ * {@see CalendarType}, e.g. {@code CalendarType#CHINESE}.
+ */
+ @NonNull
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @CalendarType.CalendarTypes
+ public static String getCalendarType(@NonNull Locale locale) {
+ return getCalendarType(locale, true);
+ }
+
+ /**
+ * Return the user's preference of the calendar type which is from {@link
+ * Locale#getDefault(Category)}, e.g. {@code CalendarType#CHINESE}.
+ *
+ * @param resolved If the {@code Locale#getDefault(Locale.Category)} contains calendar type
+ * subtag, this argument is ignored. If the
+ * {@code Locale#getDefault(Locale.Category)} doesn't contain calendar type
+ * subtag and the resolved argument is true, this function tries to find
+ * the default calendar type for the
+ * {@code Locale#getDefault(Locale.Category)}. If the
+ * {@code Locale#getDefault(Locale.Category)} doesn't contain calendar type
+ * subtag and the resolved argument is false, this function returns empty string
+ * , i.e. {@code CalendarType#DEFAULT}.
+ * @return {@link CalendarType.CalendarTypes} If the malformed calendar type format was
+ * specified in the calendar type subtag, e.g. en-US-u-ca-calendar, this function returns
+ * empty string, i.e. {@code CalendarType#DEFAULT}.
+ */
+ @NonNull
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @CalendarType.CalendarTypes
+ public static String getCalendarType(boolean resolved) {
+ Locale defaultLocale = (Build.VERSION.SDK_INT >= VERSION_CODES.N)
+ ? Api24Impl.getDefaultLocale()
+ : getDefaultLocale();
+ return getCalendarType(defaultLocale, resolved);
+ }
+
+ /**
+ * Return the calendar type of the inputted {@link Locale}, e.g. {@code CalendarType#CHINESE}.
+ *
+ * @param locale The {@link Locale} to get the calendar type.
+ * @param resolved If the given {@code Locale} contains calendar type subtag, this argument is
+ * ignored. If the given {@code Locale} doesn't contain calendar type subtag and
+ * the resolved argument is true, this function tries to find the default
+ * calendar type for the given {@code Locale}. If the given {@code Locale}
+ * doesn't contain calendar type subtag and the resolved argument is false, this
+ * function return empty string, i.e. {@code CalendarType#DEFAULT}.
+ * @return {@link CalendarType.CalendarTypes} If the malformed calendar type format was
+ * specified in the calendar type subtag, e.g. en-US-u-ca-calendar, this function returns
+ * empty string, i.e. {@code CalendarType#DEFAULT}.
+ */
+ @NonNull
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @CalendarType.CalendarTypes
+ public static String getCalendarType(@NonNull Locale locale, boolean resolved) {
+ String result = getUnicodeLocaleType(CalendarType.U_EXTENSION_TAG,
+ CalendarType.DEFAULT, locale, resolved);
+ if (result != null) {
+ return result;
+ }
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
+ return Api24Impl.getCalendarType(locale);
+ } else {
+ return resolved ? CalendarType.GREGORIAN : CalendarType.DEFAULT;
+ }
+ }
+
+ /** APIs to get the user's preference of temperature unit. */
+ public static class TemperatureUnit {
+ private static final String U_EXTENSION_TAG = "mu";
+ /** Celsius */
+ public static final String CELSIUS = "celsius";
+ /** Fahrenheit */
+ public static final String FAHRENHEIT = "fahrenhe";
+ /** Kelvin */
+ public static final String KELVIN = "kelvin";
+ /** Default Temperature for the locale */
+ public static final String DEFAULT = "";
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @StringDef({
+ CELSIUS,
+ FAHRENHEIT,
+ KELVIN,
+ DEFAULT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TemperatureUnits {
+ }
+
+ private TemperatureUnit() {
+ }
+ }
+
+ /**
+ * Return the user's preference of the temperature unit which is from {@link
+ * Locale#getDefault(Locale.Category)}. The returned result is resolved and bases on the
+ * {@code Locale#getDefault(Locale.Category)} settings. It is one of the strings defined in
+ * {@see TemperatureUnit}, e.g. {@code TemperatureUnit#FAHRENHEIT}.
+ */
+ @NonNull
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @TemperatureUnit.TemperatureUnits
+ public static String getTemperatureUnit() {
+ return getTemperatureUnit(true);
+ }
+
+ /**
+ * Return the temperature unit of the inputted {@link Locale}. It is one of the strings
+ * defined in {@see TemperatureUnit}, e.g. {@code TemperatureUnit#FAHRENHEIT}.
+ */
+ @NonNull
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @TemperatureUnit.TemperatureUnits
+ public static String getTemperatureUnit(
+ @NonNull Locale locale) {
+ return getTemperatureUnit(locale, true);
+ }
+
+ /**
+ * Return the user's preference of the temperature unit which is from {@link
+ * Locale#getDefault(Locale.Category)}, e.g. {@code TemperatureUnit#FAHRENHEIT}.
+ *
+ * @param resolved If the {@code Locale#getDefault(Locale.Category)} contains temperature unit
+ * subtag, this argument is ignored. If the
+ * {@code Locale#getDefault(Locale.Category)} doesn't contain temperature unit
+ * subtag and the resolved argument is true, this function tries to find
+ * the default temperature unit for the
+ * {@code Locale#getDefault(Locale.Category)}. If the
+ * {@code Locale#getDefault(Locale.Category)} doesn't contain temperature unit
+ * subtag and the resolved argument is false, this function returns empty string
+ * , i.e. {@code TemperatureUnit#DEFAULT}.
+ * @return {@link TemperatureUnit.TemperatureUnits} If the malformed temperature unit format was
+ * specified in the temperature unit subtag, e.g. en-US-u-mu-temperature, this function returns
+ * empty string, i.e. {@code TemperatureUnit#DEFAULT}.
+ */
+ @NonNull
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @TemperatureUnit.TemperatureUnits
+ public static String getTemperatureUnit(boolean resolved) {
+ Locale defaultLocale = (Build.VERSION.SDK_INT >= VERSION_CODES.N)
+ ? Api24Impl.getDefaultLocale()
+ : getDefaultLocale();
+ return getTemperatureUnit(defaultLocale, resolved);
+ }
+
+ /**
+ * Return the temperature unit of the inputted {@link Locale}. E.g. "fahrenheit"
+ *
+ * @param locale The {@link Locale} to get the temperature unit.
+ * @param resolved If the given {@code Locale} contains temperature unit subtag, this argument
+ * is ignored. If the given {@code Locale} doesn't contain temperature unit
+ * subtag and the resolved argument is true, this function tries to find
+ * the default temperature unit for the given {@code Locale}. If the given
+ * {@code Locale} doesn't contain temperature unit subtag and the resolved
+ * argument is false, this function return empty string, i.e.
+ * {@code TemperatureUnit#DEFAULT}.
+ * @return {@link TemperatureUnit.TemperatureUnits} If the malformed temperature unit format was
+ * specified in the temperature unit subtag, e.g. en-US-u-mu-temperature, this function returns
+ * empty string, i.e. {@code TemperatureUnit#DEFAULT}.
+ */
+ @NonNull
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @TemperatureUnit.TemperatureUnits
+ public static String getTemperatureUnit(@NonNull Locale locale, boolean resolved) {
+ String result = getUnicodeLocaleType(TemperatureUnit.U_EXTENSION_TAG,
+ TemperatureUnit.DEFAULT, locale, resolved);
+ if (result != null) {
+ return result;
+ }
+ if (BuildCompat.isAtLeastT()) {
+ return Api33Impl.getResolvedTemperatureUnit(locale);
+ } else {
+ return getTemperatureHardCoded(locale);
+ }
+ }
+
+ /** APIs to get the user's preference of the first day of week. */
+ public static class FirstDayOfWeek {
+ private static final String U_EXTENSION_TAG = "fw";
+ /** Sunday */
+ public static final String SUNDAY = "sun";
+ /** Monday */
+ public static final String MONDAY = "mon";
+ /** Tuesday */
+ public static final String TUESDAY = "tue";
+ /** Wednesday */
+ public static final String WEDNESDAY = "wed";
+ /** Thursday */
+ public static final String THURSDAY = "thu";
+ /** Friday */
+ public static final String FRIDAY = "fri";
+ /** Saturday */
+ public static final String SATURDAY = "sat";
+ /** Default first day of week for the locale */
+ public static final String DEFAULT = "";
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @StringDef({
+ SUNDAY,
+ MONDAY,
+ TUESDAY,
+ WEDNESDAY,
+ THURSDAY,
+ FRIDAY,
+ SATURDAY,
+ DEFAULT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Days {
+ }
+
+ private FirstDayOfWeek() {
+ }
+ }
+
+ /**
+ * Return the user's preference of the first day of week which is from
+ * {@link Locale#getDefault(Locale.Category)}. The returned result is resolved and bases on the
+ * {@code Locale#getDefault(Locale.Category)} settings. It is one of the strings defined in
+ * {@see FirstDayOfWeek}, e.g. {@code FirstDayOfWeek#SUNDAY}.
+ */
+ @NonNull
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @FirstDayOfWeek.Days
+ public static String getFirstDayOfWeek() {
+ return getFirstDayOfWeek(true);
+ }
+
+ /**
+ * Return the first day of week of the inputted {@link Locale}. The returned result is resolved
+ * and based on the input {@code Locale} settings. It is one of the strings defined in
+ * {@see FirstDayOfWeek}, e.g. {@code FirstDayOfWeek#SUNDAY}.
+ */
+ @NonNull
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @FirstDayOfWeek.Days
+ public static String getFirstDayOfWeek(@NonNull Locale locale) {
+ return getFirstDayOfWeek(locale, true);
+ }
+
+ /**
+ * Return the user's preference of the first day of week which is from {@link
+ * Locale#getDefault(Locale.Category)}, e.g. {@code FirstDayOfWeek#SUNDAY}.
+ *
+ * @param resolved If the {@code Locale#getDefault(Locale.Category)} contains first day of week
+ * subtag, this argument is ignored. If the
+ * {@code Locale#getDefault(Locale.Category)} doesn't contain first day of week
+ * subtag and the resolved argument is true, this function tries to find
+ * the default first day of week for the
+ * {@code Locale#getDefault(Locale.Category)}. If the
+ * {@code Locale#getDefault(Locale.Category)} doesn't contain first day of week
+ * subtag and the resolved argument is false, this function returns empty string
+ * , i.e. {@code FirstDayOfWeek#DEFAULT}.
+ * @return {@link FirstDayOfWeek.Days} If the malformed first day of week format was specified
+ * in the first day of week subtag, e.g. en-US-u-fw-days, this function returns empty string,
+ * i.e. {@code FirstDayOfWeek#DEFAULT}.
+ */
+ @NonNull
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @FirstDayOfWeek.Days
+ public static String getFirstDayOfWeek(boolean resolved) {
+ Locale defaultLocale = (Build.VERSION.SDK_INT >= VERSION_CODES.N)
+ ? Api24Impl.getDefaultLocale()
+ : getDefaultLocale();
+ return getFirstDayOfWeek(defaultLocale, resolved);
+ }
+
+ /**
+ * Return the first day of week of the inputted {@link Locale},
+ * e.g. {@code FirstDayOfWeek#SUNDAY}.
+ *
+ * @param locale The {@link Locale} to get the first day of week.
+ * @param resolved If the given {@code Locale} contains first day of week subtag, this argument
+ * is ignored. If the given {@code Locale} doesn't contain first day of week
+ * subtag and the resolved argument is true, this function tries to find
+ * the default first day of week for the given {@code Locale}. If the given
+ * {@code Locale} doesn't contain first day of week subtag and the resolved
+ * argument is false, this function return empty string, i.e.
+ * {@code FirstDayOfWeek#DEFAULT}.
+ * @return {@link FirstDayOfWeek.Days} If the malformed first day of week format was
+ * specified in the first day of week subtag, e.g. en-US-u-fw-days, this function returns
+ * empty string, i.e. {@code FirstDayOfWeek#DEFAULT}.
+ */
+ @NonNull
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @FirstDayOfWeek.Days
+ public static String getFirstDayOfWeek(
+ @NonNull Locale locale, boolean resolved) {
+ String result = getUnicodeLocaleType(FirstDayOfWeek.U_EXTENSION_TAG,
+ FirstDayOfWeek.DEFAULT, locale, resolved);
+ return result != null ? result : getBaseFirstDayOfWeek(locale);
+ }
+
+ private static String getUnicodeLocaleType(String tag, String defaultValue, Locale locale,
+ boolean resolved) {
+ String ext = locale.getUnicodeLocaleType(tag);
+ if (ext != null) {
+ return ext;
+ }
+ if (!resolved) {
+ return defaultValue;
+ }
+ return null;
+ }
+
+
+ // Warning: This list of country IDs must be in alphabetical order for binarySearch to
+ // work correctly.
+ private static final String[] WEATHER_FAHRENHEIT_COUNTRIES =
+ {"BS", "BZ", "KY", "PR", "PW", "US"};
+
+ @TemperatureUnit.TemperatureUnits
+ private static String getTemperatureHardCoded(Locale locale) {
+ return Arrays.binarySearch(WEATHER_FAHRENHEIT_COUNTRIES, locale.getCountry()) >= 0
+ ? TemperatureUnit.FAHRENHEIT
+ : TemperatureUnit.CELSIUS;
+ }
+
+ @HourCycle.HourCycleTypes
+ private static String getBaseHourCycle(@NonNull Locale locale) {
+ String pattern =
+ android.text.format.DateFormat.getBestDateTimePattern(
+ locale, "jm");
+ return pattern.contains("H") ? HourCycle.H23 : HourCycle.H12;
+ }
+
+ @FirstDayOfWeek.Days
+ private static String getBaseFirstDayOfWeek(@NonNull Locale locale) {
+ // A known bug affects both the {@code android.icu.util.Calendar} and
+ // {@code java.util.Calendar}: they ignore the "fw" field in the -u- extension, even if
+ // present. So please do not remove the explicit check on getUnicodeLocaleType,
+ // which protects us from that bug.
+ return getStringOfFirstDayOfWeek(
+ java.util.Calendar.getInstance(locale).getFirstDayOfWeek());
+ }
+
+ private static String getStringOfFirstDayOfWeek(int fw) {
+ String[] arrDays = {
+ FirstDayOfWeek.SUNDAY,
+ FirstDayOfWeek.MONDAY,
+ FirstDayOfWeek.TUESDAY,
+ FirstDayOfWeek.WEDNESDAY,
+ FirstDayOfWeek.THURSDAY,
+ FirstDayOfWeek.FRIDAY,
+ FirstDayOfWeek.SATURDAY};
+ return fw >= 1 && fw <= 7 ? arrDays[fw - 1] : FirstDayOfWeek.DEFAULT;
+ }
+
+ private static Locale getDefaultLocale() {
+ return Locale.getDefault();
+ }
+
+ @RequiresApi(VERSION_CODES.N)
+ private static class Api24Impl {
+ @DoNotInline
+ @CalendarType.CalendarTypes
+ static String getCalendarType(@NonNull Locale locale) {
+ return android.icu.util.Calendar.getInstance(locale).getType();
+ }
+
+ @DoNotInline
+ static Locale getDefaultLocale() {
+ return Locale.getDefault(Category.FORMAT);
+ }
+
+ private Api24Impl() {
+ }
+ }
+
+ @RequiresApi(VERSION_CODES.TIRAMISU)
+ private static class Api33Impl {
+ @DoNotInline
+ @TemperatureUnit.TemperatureUnits
+ static String getResolvedTemperatureUnit(@NonNull Locale locale) {
+ LocalizedNumberFormatter nf = NumberFormatter.with()
+ .usage("weather")
+ .unit(MeasureUnit.CELSIUS)
+ .locale(locale);
+ String unit = nf.format(1).getOutputUnit().getIdentifier();
+ if (unit.startsWith(TemperatureUnit.FAHRENHEIT)) {
+ return TemperatureUnit.FAHRENHEIT;
+ }
+ return unit;
+ }
+
+ @DoNotInline
+ @HourCycle.HourCycleTypes
+ static String getHourCycle(@NonNull Locale locale) {
+ return getHourCycleType(
+ DateTimePatternGenerator.getInstance(locale).getDefaultHourCycle());
+ }
+
+ @HourCycle.HourCycleTypes
+ private static String getHourCycleType(
+ DateFormat.HourCycle hourCycle) {
+ switch (hourCycle) {
+ case HOUR_CYCLE_11:
+ return HourCycle.H11;
+ case HOUR_CYCLE_12:
+ return HourCycle.H12;
+ case HOUR_CYCLE_23:
+ return HourCycle.H23;
+ case HOUR_CYCLE_24:
+ return HourCycle.H24;
+ default:
+ return HourCycle.DEFAULT;
+ }
+ }
+
+ private Api33Impl() {
+ }
+ }
+
+ private LocalePreferences() {
+ }
+}
diff --git a/core/core/src/main/java/androidx/core/util/Function.java b/core/core/src/main/java/androidx/core/util/Function.java
new file mode 100644
index 0000000..682c961
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/util/Function.java
@@ -0,0 +1,32 @@
+/*
+ * 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.core.util;
+
+/**
+ * Compat version of {@link java.util.function.Function}
+ * @param <T> the type of the input to the operation
+ * @param <R>: the type of the output of the function
+ */
+@FunctionalInterface
+public interface Function<T, R> {
+ /**
+ * Applies the function to the argument parameter.
+ *
+ * @param t the argument for the function
+ * @return the result after applying function
+ */
+ R apply(T t);
+}
diff --git a/core/core/src/main/java/androidx/core/util/TypedValueCompat.java b/core/core/src/main/java/androidx/core/util/TypedValueCompat.java
new file mode 100644
index 0000000..49f3bab
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/util/TypedValueCompat.java
@@ -0,0 +1,176 @@
+/*
+ * 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.core.util;
+
+import static android.util.TypedValue.COMPLEX_UNIT_DIP;
+import static android.util.TypedValue.COMPLEX_UNIT_IN;
+import static android.util.TypedValue.COMPLEX_UNIT_MM;
+import static android.util.TypedValue.COMPLEX_UNIT_PT;
+import static android.util.TypedValue.COMPLEX_UNIT_PX;
+import static android.util.TypedValue.COMPLEX_UNIT_SP;
+
+import android.os.Build;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * Container for a dynamically typed data value. Primarily used with
+ * {@link android.content.res.Resources} for holding resource values.
+ *
+ * <p>Used to convert between dimension values like DP and SP to pixels, and vice versa.
+ */
+public class TypedValueCompat {
+ private static final float INCHES_PER_PT = (1.0f / 72);
+ private static final float INCHES_PER_MM = (1.0f / 25.4f);
+
+ private TypedValueCompat() {}
+
+ /**
+ * Converts a pixel value to the given dimension, e.g. PX to DP.
+ *
+ * <p>This is the inverse of {@link TypedValue#applyDimension(int, float, DisplayMetrics)}
+ *
+ * @param unitToConvertTo The unit to convert to.
+ * @param pixelValue The raw pixels value to convert from.
+ * @param metrics Current display metrics to use in the conversion --
+ * supplies display density and scaling information.
+ *
+ * @return A dimension value equivalent to the given number of pixels
+ * @throws IllegalArgumentException if unitToConvertTo is not valid.
+ */
+ public static float deriveDimension(
+ int unitToConvertTo,
+ float pixelValue,
+ @NonNull DisplayMetrics metrics) {
+ if (Build.VERSION.SDK_INT >= 34) {
+ return Api34Impl.deriveDimension(unitToConvertTo, pixelValue, metrics);
+ }
+
+ switch (unitToConvertTo) {
+ case COMPLEX_UNIT_PX:
+ return pixelValue;
+ case COMPLEX_UNIT_DIP: {
+ // Avoid divide-by-zero, and return 0 since that's what the inverse function will do
+ if (metrics.density == 0) {
+ return 0;
+ }
+ return pixelValue / metrics.density;
+ }
+ case COMPLEX_UNIT_SP:
+ // Versions earlier than U don't get the fancy non-linear scaling
+ if (metrics.scaledDensity == 0) {
+ return 0;
+ }
+ return pixelValue / metrics.scaledDensity;
+ case COMPLEX_UNIT_PT: {
+ if (metrics.xdpi == 0) {
+ return 0;
+ }
+ return pixelValue / metrics.xdpi / INCHES_PER_PT;
+ }
+ case COMPLEX_UNIT_IN: {
+ if (metrics.xdpi == 0) {
+ return 0;
+ }
+ return pixelValue / metrics.xdpi;
+ }
+ case COMPLEX_UNIT_MM: {
+ if (metrics.xdpi == 0) {
+ return 0;
+ }
+ return pixelValue / metrics.xdpi / INCHES_PER_MM;
+ }
+ default:
+ throw new IllegalArgumentException("Invalid unitToConvertTo " + unitToConvertTo);
+ }
+ }
+
+ /**
+ * Converts a density-independent pixels (DP) value to pixels
+ *
+ * <p>This is a convenience function for
+ * {@link TypedValue#applyDimension(int, float, DisplayMetrics)}
+ *
+ * @param dpValue The value in DP to convert from.
+ * @param metrics Current display metrics to use in the conversion --
+ * supplies display density and scaling information.
+ *
+ * @return A raw pixel value
+ */
+ public static float dpToPx(float dpValue, @NonNull DisplayMetrics metrics) {
+ return TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, metrics);
+ }
+
+ /**
+ * Converts a pixel value to density-independent pixels (DP)
+ *
+ * <p>This is a convenience function for {@link #deriveDimension(int, float, DisplayMetrics)}
+ *
+ * @param pixelValue The raw pixels value to convert from.
+ * @param metrics Current display metrics to use in the conversion --
+ * supplies display density and scaling information.
+ *
+ * @return A dimension value (in DP) representing the given number of pixels.
+ */
+ public static float pxToDp(float pixelValue, @NonNull DisplayMetrics metrics) {
+ return deriveDimension(COMPLEX_UNIT_DIP, pixelValue, metrics);
+ }
+
+ /**
+ * Converts a scaled pixels (SP) value to pixels
+ *
+ * <p>This is a convenience function for
+ * {@link TypedValue#applyDimension(int, float, DisplayMetrics)}
+ *
+ * @param spValue The value in SP to convert from.
+ * @param metrics Current display metrics to use in the conversion --
+ * supplies display density and scaling information.
+ *
+ * @return A raw pixel value
+ */
+ public static float spToPx(float spValue, @NonNull DisplayMetrics metrics) {
+ return TypedValue.applyDimension(COMPLEX_UNIT_SP, spValue, metrics);
+ }
+
+ /**
+ * Converts a pixel value to scaled pixels (SP)
+ *
+ * <p>This is a convenience function for {@link #deriveDimension(int, float, DisplayMetrics)}
+ *
+ * @param pixelValue The raw pixels value to convert from.
+ * @param metrics Current display metrics to use in the conversion --
+ * supplies display density and scaling information.
+ *
+ * @return A dimension value (in SP) representing the given number of pixels.
+ */
+ public static float pxToSp(float pixelValue, @NonNull DisplayMetrics metrics) {
+ return deriveDimension(COMPLEX_UNIT_SP, pixelValue, metrics);
+ }
+
+ @RequiresApi(34)
+ private static class Api34Impl {
+ @DoNotInline
+ public static float deriveDimension(int unitToConvertTo, float pixelValue,
+ DisplayMetrics metrics) {
+ return TypedValue.deriveDimension(unitToConvertTo, pixelValue, metrics);
+ }
+ }
+}
diff --git a/core/core/src/main/java/androidx/core/view/VelocityTrackerCompat.java b/core/core/src/main/java/androidx/core/view/VelocityTrackerCompat.java
index a0d31d1..688e887f 100644
--- a/core/core/src/main/java/androidx/core/view/VelocityTrackerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/VelocityTrackerCompat.java
@@ -16,18 +16,36 @@
package androidx.core.view;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.os.Build;
+import android.view.MotionEvent;
import android.view.VelocityTracker;
-/**
- * Helper for accessing features in {@link VelocityTracker}.
- *
- * @deprecated Use {@link VelocityTracker} directly.
- */
-@Deprecated
+import androidx.annotation.DoNotInline;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+
+/** Helper for accessing features in {@link VelocityTracker}. */
public final class VelocityTrackerCompat {
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ @Retention(SOURCE)
+ @IntDef(value = {
+ MotionEvent.AXIS_X,
+ MotionEvent.AXIS_Y,
+ MotionEvent.AXIS_SCROLL
+ })
+ public @interface VelocityTrackableMotionEventAxis {}
/**
* Call {@link VelocityTracker#getXVelocity(int)}.
- * If running on a pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} device,
+ * If running on a pre-{@link Build.VERSION_CODES#HONEYCOMB} device,
* returns {@link VelocityTracker#getXVelocity()}.
*
* @deprecated Use {@link VelocityTracker#getXVelocity(int)} directly.
@@ -39,7 +57,7 @@
/**
* Call {@link VelocityTracker#getYVelocity(int)}.
- * If running on a pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} device,
+ * If running on a pre-{@link Build.VERSION_CODES#HONEYCOMB} device,
* returns {@link VelocityTracker#getYVelocity()}.
*
* @deprecated Use {@link VelocityTracker#getYVelocity(int)} directly.
@@ -49,5 +67,119 @@
return tracker.getYVelocity(pointerId);
}
+ /**
+ * Checks whether a given velocity-trackable {@link MotionEvent} axis is supported for velocity
+ * tracking by this {@link VelocityTracker} instance (refer to
+ * {@link #getAxisVelocity(VelocityTracker, int, int)} for a list of potentially
+ * velocity-trackable axes).
+ *
+ * <p>Note that the value returned from this method will stay the same for a given instance, so
+ * a single check for axis support is enough per a {@link VelocityTracker} instance.
+ *
+ * @param tracker The {@link VelocityTracker} for which to check axis support.
+ * @param axis The axis to check for velocity support.
+ * @return {@code true} if {@code axis} is supported for velocity tracking, or {@code false}
+ * otherwise.
+ * @see #getAxisVelocity(VelocityTracker, int, int)
+ * @see #getAxisVelocity(VelocityTracker, int)
+ */
+ public static boolean isAxisSupported(@NonNull VelocityTracker tracker,
+ @VelocityTrackableMotionEventAxis int axis) {
+ if (Build.VERSION.SDK_INT >= 34) {
+ return Api34Impl.isAxisSupported(tracker, axis);
+ }
+ return axis == MotionEvent.AXIS_X || axis == MotionEvent.AXIS_Y;
+ }
+
+ /**
+ * Equivalent to calling {@link #getAxisVelocity(VelocityTracker, int, int)} for {@code axis}
+ * and the active pointer.
+ *
+ * @param tracker The {@link VelocityTracker} from which to get axis velocity.
+ * @param axis Which axis' velocity to return.
+ * @return The previously computed velocity for {@code axis} for the active pointer if
+ * {@code axis} is supported for velocity tracking, or 0 if velocity tracking is not
+ * supported for the axis.
+ * @see #isAxisSupported(VelocityTracker, int)
+ * @see #getAxisVelocity(VelocityTracker, int, int)
+ */
+ public static float getAxisVelocity(@NonNull VelocityTracker tracker,
+ @VelocityTrackableMotionEventAxis int axis) {
+ if (Build.VERSION.SDK_INT >= 34) {
+ return Api34Impl.getAxisVelocity(tracker, axis);
+ }
+ if (axis == MotionEvent.AXIS_X) {
+ return tracker.getXVelocity();
+ }
+ if (axis == MotionEvent.AXIS_Y) {
+ return tracker.getYVelocity();
+ }
+ return 0;
+ }
+
+ /**
+ * Retrieve the last computed velocity for a given motion axis. You must first call
+ * {@link VelocityTracker#computeCurrentVelocity(int)} or
+ * {@link VelocityTracker#computeCurrentVelocity(int, float)} before calling this function.
+ *
+ * <p>In addition to {@link MotionEvent#AXIS_X} and {@link MotionEvent#AXIS_Y} which have been
+ * supported since the introduction of this class, the following axes can be candidates for this
+ * method:
+ * <ul>
+ * <li> {@link MotionEvent#AXIS_SCROLL}: supported starting
+ * {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}
+ * </ul>
+ *
+ * <p>Before accessing velocities of an axis using this method, check that your
+ * {@link VelocityTracker} instance supports the axis by using
+ * {@link #isAxisSupported(VelocityTracker, int)}.
+ *
+ * @param tracker The {@link VelocityTracker} from which to get axis velocity.
+ * @param axis Which axis' velocity to return.
+ * @param pointerId Which pointer's velocity to return.
+ * @return The previously computed velocity for {@code axis} for pointer ID of {@code id} if
+ * {@code axis} is supported for velocity tracking, or 0 if velocity tracking is not
+ * supported for the axis.
+ * @see #isAxisSupported(VelocityTracker, int)
+ */
+ public static float getAxisVelocity(
+ @NonNull VelocityTracker tracker,
+ @VelocityTrackableMotionEventAxis int axis,
+ int pointerId) {
+ if (Build.VERSION.SDK_INT >= 34) {
+ return Api34Impl.getAxisVelocity(tracker, axis, pointerId);
+ }
+ if (axis == MotionEvent.AXIS_X) {
+ return tracker.getXVelocity(pointerId);
+ }
+ if (axis == MotionEvent.AXIS_Y) {
+ return tracker.getYVelocity(pointerId);
+ }
+ return 0;
+
+ }
+
+ @RequiresApi(34)
+ private static class Api34Impl {
+ private Api34Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static boolean isAxisSupported(VelocityTracker velocityTracker, int axis) {
+ return velocityTracker.isAxisSupported(axis);
+ }
+
+ @DoNotInline
+ static float getAxisVelocity(VelocityTracker velocityTracker, int axis, int id) {
+ return velocityTracker.getAxisVelocity(axis, id);
+ }
+
+ @DoNotInline
+ static float getAxisVelocity(VelocityTracker velocityTracker, int axis) {
+ return velocityTracker.getAxisVelocity(axis);
+ }
+ }
+
private VelocityTrackerCompat() {}
}
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
index 3157094..957d459 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
@@ -169,6 +169,11 @@
public static final int TYPE_ASSIST_READING_CONTEXT = 0x01000000;
/**
+ * Represents the event of a scroll having completed and brought the target node on screen.
+ */
+ public static final int TYPE_VIEW_TARGETED_BY_SCROLL = 0x04000000;
+
+ /**
* Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
* The type of change is not defined.
*/
@@ -270,6 +275,7 @@
* @see AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED
* @see AccessibilityEvent#TYPE_VIEW_SCROLLED
* @see AccessibilityEvent#TYPE_VIEW_TEXT_SELECTION_CHANGED
+ * @see AccessibilityEvent#TYPE_VIEW_TARGETED_BY_SCROLL
* @see #TYPE_ANNOUNCEMENT
* @see #TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY
* @see #TYPE_GESTURE_DETECTION_START
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
index 3837165..82267bc 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
@@ -654,6 +654,40 @@
: null, android.R.id.accessibilityActionShowTextSuggestions, null,
null, null);
+ /**
+ * Action that brings fully on screen the next node in the specified direction.
+ *
+ * <p>
+ * This should include wrapping around to the next/previous row, column, etc. in a
+ * collection if one is available. If there is no node in that direction, the action
+ * should fail and return false.
+ * </p>
+ * <p>
+ * This action should be used instead of
+ * {@link AccessibilityActionCompat#ACTION_SCROLL_TO_POSITION} when a widget does not
+ * have clear row and column semantics or if a directional search is needed to find a
+ * node in a complex ViewGroup where individual nodes may span multiple rows or
+ * columns. The implementing widget must send a
+ * {@link AccessibilityEventCompat#TYPE_VIEW_TARGETED_BY_SCROLL} accessibility event
+ * with the scroll target as the source. An accessibility service can listen for this
+ * event, inspect its source, and use the result when determining where to place
+ * accessibility focus.
+ * <p>
+ * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_DIRECTION_INT}. This is a
+ * required argument.<br>
+ * </p>
+ */
+ @NonNull
+ @OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
+ public static final AccessibilityActionCompat ACTION_SCROLL_IN_DIRECTION =
+ new AccessibilityActionCompat(BuildCompat.isAtLeastU()
+ ? AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_IN_DIRECTION
+ : null,
+ // TODO (267511848): update ID value once U resources are finalized.
+ BuildCompat.isAtLeastU()
+ ? android.R.id.accessibilityActionScrollInDirection : -1,
+ null, null, null);
+
final Object mAction;
private final int mId;
private final Class<? extends CommandArguments> mViewCommandArgumentClass;
@@ -1751,6 +1785,25 @@
public static final String ACTION_ARGUMENT_PRESS_AND_HOLD_DURATION_MILLIS_INT =
"android.view.accessibility.action.ARGUMENT_PRESS_AND_HOLD_DURATION_MILLIS_INT";
+ /**
+ * <p>Argument to represent the direction when using
+ * {@link AccessibilityActionCompat#ACTION_SCROLL_IN_DIRECTION}.</p>
+ *
+ * <p>
+ * The value of this argument can be one of:
+ * <ul>
+ * <li>{@link View#FOCUS_DOWN}</li>
+ * <li>{@link View#FOCUS_UP}</li>
+ * <li>{@link View#FOCUS_LEFT}</li>
+ * <li>{@link View#FOCUS_RIGHT}</li>
+ * <li>{@link View#FOCUS_FORWARD}</li>
+ * <li>{@link View#FOCUS_BACKWARD}</li>
+ * </ul>
+ * </p>
+ */
+ public static final String ACTION_ARGUMENT_DIRECTION_INT =
+ "androidx.core.view.accessibility.action.ARGUMENT_DIRECTION_INT";
+
// Focus types
/**
@@ -4609,6 +4662,11 @@
case android.R.id.accessibilityActionDragCancel:
return "ACTION_DRAG_CANCEL";
default:
+ // TODO (b/267511848): fix after Android U constants are finalized.
+ if (Build.VERSION.SDK_INT >= 34
+ && action == android.R.id.accessibilityActionScrollInDirection) {
+ return "ACTION_SCROLL_IN_DIRECTION";
+ }
return "ACTION_UNKNOWN";
}
}
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompat.java
index 511d9b8..91586a5 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompat.java
@@ -87,6 +87,25 @@
return null;
}
+ /**
+ * Creates a new AccessibilityWindowInfoCompat.
+ * <p>
+ * Compatibility:
+ * <ul>
+ * <li>Api < 30: Will not wrap an
+ * {@link android.view.accessibility.AccessibilityWindowInfo} instance.</li>
+ * </ul>
+ * </p>
+ *
+ */
+ public AccessibilityWindowInfoCompat() {
+ if (SDK_INT >= 30) {
+ mInfo = Api30Impl.instantiateAccessibilityWindowInfo();
+ } else {
+ mInfo = null;
+ }
+ }
+
private AccessibilityWindowInfoCompat(Object info) {
mInfo = info;
}
@@ -541,6 +560,18 @@
}
}
+ @RequiresApi(30)
+ private static class Api30Impl {
+ private Api30Impl() {
+ // This class is non instantiable.
+ }
+
+ @DoNotInline
+ static AccessibilityWindowInfo instantiateAccessibilityWindowInfo() {
+ return new AccessibilityWindowInfo();
+ }
+ }
+
@RequiresApi(33)
private static class Api33Impl {
private Api33Impl() {
diff --git a/core/core/src/main/java/androidx/core/view/contentcapture/ContentCaptureSessionCompat.java b/core/core/src/main/java/androidx/core/view/contentcapture/ContentCaptureSessionCompat.java
index a672caa..c5c89d7 100644
--- a/core/core/src/main/java/androidx/core/view/contentcapture/ContentCaptureSessionCompat.java
+++ b/core/core/src/main/java/androidx/core/view/contentcapture/ContentCaptureSessionCompat.java
@@ -246,8 +246,7 @@
@DoNotInline
static void notifyViewsAppeared(
ContentCaptureSession contentCaptureSession, List<ViewStructure> appearedNodes) {
- // new API in U
- // contentCaptureSession.notifyViewsAppeared(appearedNodes);
+ contentCaptureSession.notifyViewsAppeared(appearedNodes);
}
}
@RequiresApi(29)
diff --git a/core/core/src/test/resources/robolectric.properties b/core/core/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..69fde47
--- /dev/null
+++ b/core/core/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/credentials/credentials-play-services-auth/build.gradle b/credentials/credentials-play-services-auth/build.gradle
index 4558801..dab1da2 100644
--- a/credentials/credentials-play-services-auth/build.gradle
+++ b/credentials/credentials-play-services-auth/build.gradle
@@ -68,7 +68,7 @@
}
androidx {
- name = "Credentials Play Services Auth Library"
+ name = "Credentials Play Services Auth"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2022"
description = "sign into apps using play-services-auth library"
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/beginsignin/CredentialProviderBeginSignInControllerJavaTest.java b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/beginsignin/CredentialProviderBeginSignInControllerJavaTest.java
index 969f941..45af09f 100644
--- a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/beginsignin/CredentialProviderBeginSignInControllerJavaTest.java
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/beginsignin/CredentialProviderBeginSignInControllerJavaTest.java
@@ -34,6 +34,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.HashSet;
import java.util.List;
@RunWith(AndroidJUnit4.class)
@@ -68,7 +69,7 @@
CredentialProviderBeginSignInController
.getInstance(activity)
.convertRequestToPlayServices(new GetCredentialRequest(List.of(
- new GetPasswordOption(true)
+ new GetPasswordOption(new HashSet<>(), true)
)));
assertThat(actualResponse.getPasswordRequestOptions().isSupported()).isTrue();
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/beginsignin/CredentialProviderBeginSignInControllerTest.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/beginsignin/CredentialProviderBeginSignInControllerTest.kt
index 9652061..5004960 100644
--- a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/beginsignin/CredentialProviderBeginSignInControllerTest.kt
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/beginsignin/CredentialProviderBeginSignInControllerTest.kt
@@ -66,7 +66,7 @@
.convertRequestToPlayServices(
GetCredentialRequest(
listOf(
- GetPasswordOption(true)
+ GetPasswordOption(isAutoSelectAllowed = true)
)
)
)
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
index 3524fe2..27737a3 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
@@ -16,7 +16,6 @@
package androidx.credentials.playservices
-import android.app.Activity
import android.content.Context
import android.os.CancellationSignal
import android.util.Log
@@ -54,21 +53,21 @@
@VisibleForTesting
var googleApiAvailability = GoogleApiAvailability.getInstance()
override fun onGetCredential(
+ context: Context,
request: GetCredentialRequest,
- activity: Activity,
cancellationSignal: CancellationSignal?,
executor: Executor,
callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>
) {
if (cancellationReviewer(cancellationSignal)) { return }
- CredentialProviderBeginSignInController(activity).invokePlayServices(
+ CredentialProviderBeginSignInController(context).invokePlayServices(
request, callback, executor, cancellationSignal)
}
@SuppressWarnings("deprecated")
override fun onCreateCredential(
+ context: Context,
request: CreateCredentialRequest,
- activity: Activity,
cancellationSignal: CancellationSignal?,
executor: Executor,
callback: CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>
@@ -77,7 +76,7 @@
when (request) {
is CreatePasswordRequest -> {
CredentialProviderCreatePasswordController.getInstance(
- activity).invokePlayServices(
+ context).invokePlayServices(
request,
callback,
executor,
@@ -85,7 +84,7 @@
}
is CreatePublicKeyCredentialRequest -> {
CredentialProviderCreatePublicKeyCredentialController.getInstance(
- activity).invokePlayServices(
+ context).invokePlayServices(
request,
callback,
executor,
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
index dcea304..6261942 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
@@ -16,7 +16,7 @@
package androidx.credentials.playservices.controllers.BeginSignIn
-import android.app.Activity
+import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.CancellationSignal
@@ -55,13 +55,13 @@
* @hide
*/
@Suppress("deprecation")
-class CredentialProviderBeginSignInController(private val activity: Activity) :
+class CredentialProviderBeginSignInController(private val context: Context) :
CredentialProviderController<
GetCredentialRequest,
BeginSignInRequest,
SignInCredential,
GetCredentialResponse,
- GetCredentialException>(activity) {
+ GetCredentialException>(context) {
/**
* The callback object state, used in the protected handleResponse method.
@@ -119,10 +119,10 @@
}
val convertedRequest: BeginSignInRequest = this.convertRequestToPlayServices(request)
- val hiddenIntent = Intent(activity, HiddenActivity::class.java)
+ val hiddenIntent = Intent(context, HiddenActivity::class.java)
hiddenIntent.putExtra(REQUEST_TAG, convertedRequest)
generateHiddenActivityIntent(resultReceiver, hiddenIntent, BEGIN_SIGN_IN_TAG)
- activity.startActivity(hiddenIntent)
+ context.startActivity(hiddenIntent)
}
internal fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
@@ -143,7 +143,7 @@
)
) return
try {
- val signInCredential = Identity.getSignInClient(activity)
+ val signInCredential = Identity.getSignInClient(context)
.getSignInCredentialFromIntent(data)
val response = convertResponseToCredentialManager(signInCredential)
cancelOrCallbackExceptionOrResult(cancellationSignal) {
@@ -246,14 +246,14 @@
* This finds a past version of the [CredentialProviderBeginSignInController] if it exists,
* otherwise it generates a new instance.
*
- * @param activity the calling activity for this controller
+ * @param context the calling context for this controller
* @return a credential provider controller for a specific begin sign in credential request
*/
@JvmStatic
- fun getInstance(activity: Activity):
+ fun getInstance(context: Context):
CredentialProviderBeginSignInController {
if (controller == null) {
- controller = CredentialProviderBeginSignInController(activity)
+ controller = CredentialProviderBeginSignInController(context)
}
return controller!!
}
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
index a7b6746..9c4d131 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
@@ -16,7 +16,7 @@
package androidx.credentials.playservices.controllers.CreatePassword
-import android.app.Activity
+import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.CancellationSignal
@@ -44,13 +44,13 @@
* @hide
*/
@Suppress("deprecation")
-class CredentialProviderCreatePasswordController(private val activity: Activity) :
+class CredentialProviderCreatePasswordController(private val context: Context) :
CredentialProviderController<
CreatePasswordRequest,
SavePasswordRequest,
Unit,
CreateCredentialResponse,
- CreateCredentialException>(activity) {
+ CreateCredentialException>(context) {
/**
* The callback object state, used in the protected handleResponse method.
@@ -101,10 +101,10 @@
}
val convertedRequest: SavePasswordRequest = this.convertRequestToPlayServices(request)
- val hiddenIntent = Intent(activity, HiddenActivity::class.java)
+ val hiddenIntent = Intent(context, HiddenActivity::class.java)
hiddenIntent.putExtra(REQUEST_TAG, convertedRequest)
generateHiddenActivityIntent(resultReceiver, hiddenIntent, CREATE_PASSWORD_TAG)
- activity.startActivity(hiddenIntent)
+ context.startActivity(hiddenIntent)
}
internal fun handleResponse(uniqueRequestCode: Int, resultCode: Int) {
@@ -144,14 +144,14 @@
* [CredentialProviderCreatePasswordController] if it exists, otherwise
* it generates a new instance.
*
- * @param activity the calling activity for this controller
+ * @param context the calling context for this controller
* @return a credential provider controller for CreatePasswordController
*/
@JvmStatic
- fun getInstance(activity: Activity):
+ fun getInstance(context: Context):
CredentialProviderCreatePasswordController {
if (controller == null) {
- controller = CredentialProviderCreatePasswordController(activity)
+ controller = CredentialProviderCreatePasswordController(context)
}
return controller!!
}
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt
index ac236eb..110df63 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt
@@ -16,7 +16,7 @@
package androidx.credentials.playservices.controllers.CreatePublicKeyCredential
-import android.app.Activity
+import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.CancellationSignal
@@ -50,13 +50,13 @@
* @hide
*/
@Suppress("deprecation")
-class CredentialProviderCreatePublicKeyCredentialController(private val activity: Activity) :
+class CredentialProviderCreatePublicKeyCredentialController(private val context: Context) :
CredentialProviderController<
CreatePublicKeyCredentialRequest,
PublicKeyCredentialCreationOptions,
PublicKeyCredential,
CreateCredentialResponse,
- CreateCredentialException>(activity) {
+ CreateCredentialException>(context) {
/**
* The callback object state, used in the protected handleResponse method.
@@ -121,11 +121,11 @@
if (CredentialProviderPlayServicesImpl.cancellationReviewer(cancellationSignal)) {
return
}
- val hiddenIntent = Intent(activity, HiddenActivity::class.java)
+ val hiddenIntent = Intent(context, HiddenActivity::class.java)
hiddenIntent.putExtra(REQUEST_TAG, fidoRegistrationRequest)
generateHiddenActivityIntent(resultReceiver, hiddenIntent,
CREATE_PUBLIC_KEY_CREDENTIAL_TAG)
- activity.startActivity(hiddenIntent)
+ context.startActivity(hiddenIntent)
}
internal fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
@@ -196,14 +196,14 @@
* [CredentialProviderCreatePublicKeyCredentialController] if it exists, otherwise
* it generates a new instance.
*
- * @param activity the calling activity for this controller
+ * @param context the calling context for this controller
* @return a credential provider controller for CreatePublicKeyCredential
*/
@JvmStatic
- fun getInstance(activity: Activity):
+ fun getInstance(context: Context):
CredentialProviderCreatePublicKeyCredentialController {
if (controller == null) {
- controller = CredentialProviderCreatePublicKeyCredentialController(activity)
+ controller = CredentialProviderCreatePublicKeyCredentialController(context)
}
return controller!!
}
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderBaseController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderBaseController.kt
index 7e9365d..03a254c 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderBaseController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderBaseController.kt
@@ -16,6 +16,7 @@
package androidx.credentials.playservices.controllers
+import android.content.Context
import android.content.Intent
import android.os.Parcel
import android.os.ResultReceiver
@@ -34,7 +35,7 @@
* Holds all non type specific details shared by the controllers.
* @hide
*/
-open class CredentialProviderBaseController(private val activity: android.app.Activity) {
+open class CredentialProviderBaseController(private val context: Context) {
companion object {
// Common retryable status codes from the play modules found
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt
index fafe63a..8a5dac6 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt
@@ -17,6 +17,7 @@
package androidx.credentials.playservices.controllers
import android.app.Activity
+import android.content.Context
import android.os.Bundle
import android.os.CancellationSignal
import androidx.credentials.CredentialManagerCallback
@@ -45,7 +46,7 @@
*/
@Suppress("deprecation")
abstract class CredentialProviderController<T1 : Any, T2 : Any, R2 : Any, R1 : Any,
- E1 : Any>(private val activity: Activity) : CredentialProviderBaseController(activity) {
+ E1 : Any>(private val context: Context) : CredentialProviderBaseController(context) {
companion object {
diff --git a/credentials/credentials-provider/build.gradle b/credentials/credentials-provider/build.gradle
index 0f5fb6d..a2d506c 100644
--- a/credentials/credentials-provider/build.gradle
+++ b/credentials/credentials-provider/build.gradle
@@ -36,7 +36,7 @@
}
androidx {
- name = "Credentials Provider Library"
+ name = "Credentials Provider"
publish = Publish.NONE
inceptionYear = "2022"
description = "use utility APIs to process requests from, and return responses to the android" +
diff --git a/credentials/credentials/api/api_lint.ignore b/credentials/credentials/api/api_lint.ignore
index be1a990..8d0b1d6 100644
--- a/credentials/credentials/api/api_lint.ignore
+++ b/credentials/credentials/api/api_lint.ignore
@@ -1,5 +1,7 @@
// Baseline format: 1.0
-GetterSetterNames: field CreatePublicKeyCredentialRequest.preferImmediatelyAvailableCredentials:
+GetterSetterNames: field CreateCredentialRequest.preferImmediatelyAvailableCredentials:
Invalid name for boolean property `preferImmediatelyAvailableCredentials`. Should start with one of `has`, `can`, `should`, `is`.
-GetterSetterNames: field GetPublicKeyCredentialOption.preferImmediatelyAvailableCredentials:
+GetterSetterNames: field GetCredentialRequest.preferIdentityDocUi:
+ Invalid name for boolean property `preferIdentityDocUi`. Should start with one of `has`, `can`, `should`, `is`.
+GetterSetterNames: field GetCredentialRequest.preferImmediatelyAvailableCredentials:
Invalid name for boolean property `preferImmediatelyAvailableCredentials`. Should start with one of `has`, `can`, `should`, `is`.
diff --git a/credentials/credentials/api/current.txt b/credentials/credentials/api/current.txt
index d15ea9a..cd87117 100644
--- a/credentials/credentials/api/current.txt
+++ b/credentials/credentials/api/current.txt
@@ -6,13 +6,28 @@
}
public abstract class CreateCredentialRequest {
+ 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 boolean getPreferImmediatelyAvailableCredentials();
+ method public final String getType();
+ method public final boolean isAutoSelectAllowed();
+ method public final boolean isSystemProviderRequired();
+ 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;
}
public static final class CreateCredentialRequest.DisplayInfo {
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
+ ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
method public CharSequence? getUserDisplayName();
method public CharSequence getUserId();
property public final CharSequence? userDisplayName;
@@ -20,35 +35,28 @@
}
public abstract class CreateCredentialResponse {
- }
-
- 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, 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);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
- method public final android.os.Bundle getCandidateQueryData();
- method public final android.os.Bundle getCredentialData();
- method public final String getType();
- method public final boolean isAutoSelectAllowed();
- method public final boolean isSystemProviderRequired();
- property public final android.os.Bundle candidateQueryData;
- property public final android.os.Bundle credentialData;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isSystemProviderRequired;
- property public final String type;
- }
-
- public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);
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, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
+ 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);
+ ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
+ }
+
+ 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, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
ctor public CreatePasswordRequest(String id, String password, optional String? origin);
ctor public CreatePasswordRequest(String id, String password);
+ ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials);
method public String getId();
method public String getPassword();
property public final String id;
@@ -60,15 +68,14 @@
}
public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash);
+ 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);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);
ctor public CreatePublicKeyCredentialRequest(String requestJson);
- method public String? getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, byte[]? clientDataHash, boolean preferImmediatelyAvailableCredentials, String? origin, String? preferDefaultProvider);
+ method public byte[]? getClientDataHash();
method public String getRequestJson();
- property public final String? clientDataHash;
- property public final boolean preferImmediatelyAvailableCredentials;
+ property public final byte[]? clientDataHash;
property public final String requestJson;
}
@@ -79,16 +86,25 @@
}
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 final class CredentialManager {
- method public suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ @RequiresApi(16) 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 suspend Object? createCredential(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
- method public void createCredentialAsync(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method public suspend Object? getCredential(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
- method public void getCredentialAsync(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
+ method public default 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;
}
@@ -102,30 +118,49 @@
}
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 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;
}
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(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method public void onGetCredential(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> 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);
- 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 final class GetCredentialRequest {
+ 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);
+ 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);
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);
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
method public String? getOrigin();
+ method public boolean getPreferIdentityDocUi();
+ method public boolean getPreferImmediatelyAvailableCredentials();
+ method public android.content.ComponentName? getPreferUiBrandingComponentName();
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 {
@@ -134,6 +169,9 @@
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 {
@@ -143,36 +181,27 @@
}
public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
+ 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);
ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
- method public final android.os.Bundle getCandidateQueryData();
- method public final android.os.Bundle getRequestData();
- method public final String getType();
- method public final boolean isAutoSelectAllowed();
- method public final boolean isSystemProviderRequired();
- 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;
}
public final class GetPasswordOption extends androidx.credentials.CredentialOption {
- ctor public GetPasswordOption(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);
+ 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);
ctor public GetPasswordOption();
- method public boolean isAutoSelectAllowed();
- property public boolean isAutoSelectAllowed;
+ 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, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional String? clientDataHash);
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set<android.content.ComponentName> allowedProviders);
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);
ctor public GetPublicKeyCredentialOption(String requestJson);
- method public String? getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
+ method public byte[]? getClientDataHash();
method public String getRequestJson();
- property public final String? clientDataHash;
- property public final boolean preferImmediatelyAvailableCredentials;
+ property public final byte[]? clientDataHash;
property public final String requestJson;
}
@@ -182,12 +211,34 @@
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 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();
+ 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);
}
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 {
}
}
@@ -454,3 +505,346 @@
}
+package androidx.credentials.provider {
+
+ public final class Action {
+ ctor public Action(CharSequence title, android.app.PendingIntent pendingIntent, optional CharSequence? subtitle);
+ 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;
+ }
+
+ 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 final class AuthenticationAction {
+ ctor public AuthenticationAction(CharSequence title, android.app.PendingIntent pendingIntent);
+ method public android.app.PendingIntent getPendingIntent();
+ method public CharSequence getTitle();
+ property public final android.app.PendingIntent pendingIntent;
+ property public final CharSequence title;
+ }
+
+ public abstract class BeginCreateCredentialRequest {
+ ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, android.service.credentials.CallingAppInfo? callingAppInfo);
+ method public final android.service.credentials.CallingAppInfo? getCallingAppInfo();
+ method public final android.os.Bundle getCandidateQueryData();
+ method public final String getType();
+ method public static final androidx.credentials.provider.BeginCreateCredentialRequest? readFromBundle(android.os.Bundle bundle);
+ method public static final android.os.Bundle writeToBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
+ property public final android.service.credentials.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 androidx.credentials.provider.BeginCreateCredentialRequest? readFromBundle(android.os.Bundle bundle);
+ method public android.os.Bundle writeToBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
+ }
+
+ public final class BeginCreateCredentialResponse {
+ ctor public BeginCreateCredentialResponse(optional java.util.List<androidx.credentials.provider.CreateEntry> createEntries, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
+ method public java.util.List<androidx.credentials.provider.CreateEntry> getCreateEntries();
+ method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
+ method public static androidx.credentials.provider.BeginCreateCredentialResponse? readFromBundle(android.os.Bundle bundle);
+ method public static android.os.Bundle writeToBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
+ 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 androidx.credentials.provider.BeginCreateCredentialResponse? readFromBundle(android.os.Bundle bundle);
+ method public android.os.Bundle writeToBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
+ }
+
+ public class BeginCreateCustomCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
+ ctor public BeginCreateCustomCredentialRequest(String type, android.os.Bundle candidateQueryData, android.service.credentials.CallingAppInfo? callingAppInfo);
+ }
+
+ public final class BeginCreatePasswordCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
+ ctor public BeginCreatePasswordCredentialRequest(android.service.credentials.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
+ }
+
+ public final class BeginCreatePublicKeyCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
+ ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, android.service.credentials.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);
+ ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, android.service.credentials.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
+ 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, optional android.service.credentials.CallingAppInfo? callingAppInfo);
+ ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions);
+ method public java.util.List<androidx.credentials.provider.BeginGetCredentialOption> getBeginGetCredentialOptions();
+ method public android.service.credentials.CallingAppInfo? getCallingAppInfo();
+ method public static androidx.credentials.provider.BeginGetCredentialRequest? readFromBundle(android.os.Bundle bundle);
+ method public static android.os.Bundle writeToBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
+ property public final java.util.List<androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions;
+ property public final android.service.credentials.CallingAppInfo? callingAppInfo;
+ field public static final androidx.credentials.provider.BeginGetCredentialRequest.Companion Companion;
+ }
+
+ public static final class BeginGetCredentialRequest.Companion {
+ method public androidx.credentials.provider.BeginGetCredentialRequest? readFromBundle(android.os.Bundle bundle);
+ method public android.os.Bundle writeToBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
+ }
+
+ 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 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();
+ method public static androidx.credentials.provider.BeginGetCredentialResponse? readFromBundle(android.os.Bundle bundle);
+ method public static android.os.Bundle writeToBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
+ 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 androidx.credentials.provider.BeginGetCredentialResponse? readFromBundle(android.os.Bundle bundle);
+ method public android.os.Bundle writeToBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
+ }
+
+ 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 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, optional byte[]? clientDataHash);
+ ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson);
+ method public byte[]? getClientDataHash();
+ method public String getRequestJson();
+ property public final byte[]? clientDataHash;
+ property public final String requestJson;
+ }
+
+ 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);
+ 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();
+ property public final CharSequence accountName;
+ property public final CharSequence? description;
+ property public final android.graphics.drawable.Icon? icon;
+ property public final java.time.Instant? lastUsedTime;
+ property public final android.app.PendingIntent pendingIntent;
+ }
+
+ 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 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 abstract class CredentialEntry {
+ method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
+ property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
+ }
+
+ @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(28) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+ 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);
+ 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 isAutoSelectAllowed();
+ 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;
+ property public final CharSequence? subtitle;
+ property public final CharSequence title;
+ property public String type;
+ property public final CharSequence? typeDisplayName;
+ }
+
+ 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 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);
+ }
+
+ @RequiresApi(28) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+ 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);
+ 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 isAutoSelectAllowed();
+ property public final CharSequence? displayName;
+ 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;
+ property public final CharSequence typeDisplayName;
+ property public final CharSequence username;
+ }
+
+ 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 setAutoSelectAllowed(boolean autoSelectAllowed);
+ 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);
+ }
+
+ @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(android.service.credentials.CallingAppInfo callingAppInfo);
+ method public android.service.credentials.CallingAppInfo getCallingAppInfo();
+ property public final android.service.credentials.CallingAppInfo callingAppInfo;
+ }
+
+ public final class ProviderCreateCredentialRequest {
+ ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, android.service.credentials.CallingAppInfo callingAppInfo);
+ method public android.service.credentials.CallingAppInfo getCallingAppInfo();
+ method public androidx.credentials.CreateCredentialRequest getCallingRequest();
+ property public final android.service.credentials.CallingAppInfo callingAppInfo;
+ property public final androidx.credentials.CreateCredentialRequest callingRequest;
+ }
+
+ @RequiresApi(34) public final class ProviderGetCredentialRequest {
+ ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, android.service.credentials.CallingAppInfo callingAppInfo);
+ method public android.service.credentials.CallingAppInfo getCallingAppInfo();
+ method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
+ property public final android.service.credentials.CallingAppInfo callingAppInfo;
+ property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
+ }
+
+ @RequiresApi(28) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+ 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);
+ 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 isAutoSelectAllowed();
+ property public final CharSequence? displayName;
+ 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;
+ property public final CharSequence typeDisplayName;
+ property public final CharSequence username;
+ }
+
+ 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 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 final class RemoteEntry {
+ ctor public RemoteEntry(android.app.PendingIntent pendingIntent);
+ method public android.app.PendingIntent getPendingIntent();
+ property public final android.app.PendingIntent pendingIntent;
+ }
+
+}
+
diff --git a/credentials/credentials/api/public_plus_experimental_current.txt b/credentials/credentials/api/public_plus_experimental_current.txt
index d15ea9a..cd87117 100644
--- a/credentials/credentials/api/public_plus_experimental_current.txt
+++ b/credentials/credentials/api/public_plus_experimental_current.txt
@@ -6,13 +6,28 @@
}
public abstract class CreateCredentialRequest {
+ 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 boolean getPreferImmediatelyAvailableCredentials();
+ method public final String getType();
+ method public final boolean isAutoSelectAllowed();
+ method public final boolean isSystemProviderRequired();
+ 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;
}
public static final class CreateCredentialRequest.DisplayInfo {
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
+ ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
method public CharSequence? getUserDisplayName();
method public CharSequence getUserId();
property public final CharSequence? userDisplayName;
@@ -20,35 +35,28 @@
}
public abstract class CreateCredentialResponse {
- }
-
- 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, 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);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
- method public final android.os.Bundle getCandidateQueryData();
- method public final android.os.Bundle getCredentialData();
- method public final String getType();
- method public final boolean isAutoSelectAllowed();
- method public final boolean isSystemProviderRequired();
- property public final android.os.Bundle candidateQueryData;
- property public final android.os.Bundle credentialData;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isSystemProviderRequired;
- property public final String type;
- }
-
- public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);
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, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
+ 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);
+ ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
+ }
+
+ 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, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
ctor public CreatePasswordRequest(String id, String password, optional String? origin);
ctor public CreatePasswordRequest(String id, String password);
+ ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials);
method public String getId();
method public String getPassword();
property public final String id;
@@ -60,15 +68,14 @@
}
public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash);
+ 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);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);
ctor public CreatePublicKeyCredentialRequest(String requestJson);
- method public String? getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, byte[]? clientDataHash, boolean preferImmediatelyAvailableCredentials, String? origin, String? preferDefaultProvider);
+ method public byte[]? getClientDataHash();
method public String getRequestJson();
- property public final String? clientDataHash;
- property public final boolean preferImmediatelyAvailableCredentials;
+ property public final byte[]? clientDataHash;
property public final String requestJson;
}
@@ -79,16 +86,25 @@
}
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 final class CredentialManager {
- method public suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ @RequiresApi(16) 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 suspend Object? createCredential(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
- method public void createCredentialAsync(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method public suspend Object? getCredential(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
- method public void getCredentialAsync(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
+ method public default 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;
}
@@ -102,30 +118,49 @@
}
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 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;
}
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(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method public void onGetCredential(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> 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);
- 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 final class GetCredentialRequest {
+ 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);
+ 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);
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);
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
method public String? getOrigin();
+ method public boolean getPreferIdentityDocUi();
+ method public boolean getPreferImmediatelyAvailableCredentials();
+ method public android.content.ComponentName? getPreferUiBrandingComponentName();
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 {
@@ -134,6 +169,9 @@
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 {
@@ -143,36 +181,27 @@
}
public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
+ 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);
ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
- method public final android.os.Bundle getCandidateQueryData();
- method public final android.os.Bundle getRequestData();
- method public final String getType();
- method public final boolean isAutoSelectAllowed();
- method public final boolean isSystemProviderRequired();
- 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;
}
public final class GetPasswordOption extends androidx.credentials.CredentialOption {
- ctor public GetPasswordOption(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);
+ 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);
ctor public GetPasswordOption();
- method public boolean isAutoSelectAllowed();
- property public boolean isAutoSelectAllowed;
+ 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, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional String? clientDataHash);
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set<android.content.ComponentName> allowedProviders);
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);
ctor public GetPublicKeyCredentialOption(String requestJson);
- method public String? getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
+ method public byte[]? getClientDataHash();
method public String getRequestJson();
- property public final String? clientDataHash;
- property public final boolean preferImmediatelyAvailableCredentials;
+ property public final byte[]? clientDataHash;
property public final String requestJson;
}
@@ -182,12 +211,34 @@
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 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();
+ 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);
}
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 {
}
}
@@ -454,3 +505,346 @@
}
+package androidx.credentials.provider {
+
+ public final class Action {
+ ctor public Action(CharSequence title, android.app.PendingIntent pendingIntent, optional CharSequence? subtitle);
+ 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;
+ }
+
+ 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 final class AuthenticationAction {
+ ctor public AuthenticationAction(CharSequence title, android.app.PendingIntent pendingIntent);
+ method public android.app.PendingIntent getPendingIntent();
+ method public CharSequence getTitle();
+ property public final android.app.PendingIntent pendingIntent;
+ property public final CharSequence title;
+ }
+
+ public abstract class BeginCreateCredentialRequest {
+ ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, android.service.credentials.CallingAppInfo? callingAppInfo);
+ method public final android.service.credentials.CallingAppInfo? getCallingAppInfo();
+ method public final android.os.Bundle getCandidateQueryData();
+ method public final String getType();
+ method public static final androidx.credentials.provider.BeginCreateCredentialRequest? readFromBundle(android.os.Bundle bundle);
+ method public static final android.os.Bundle writeToBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
+ property public final android.service.credentials.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 androidx.credentials.provider.BeginCreateCredentialRequest? readFromBundle(android.os.Bundle bundle);
+ method public android.os.Bundle writeToBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
+ }
+
+ public final class BeginCreateCredentialResponse {
+ ctor public BeginCreateCredentialResponse(optional java.util.List<androidx.credentials.provider.CreateEntry> createEntries, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
+ method public java.util.List<androidx.credentials.provider.CreateEntry> getCreateEntries();
+ method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
+ method public static androidx.credentials.provider.BeginCreateCredentialResponse? readFromBundle(android.os.Bundle bundle);
+ method public static android.os.Bundle writeToBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
+ 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 androidx.credentials.provider.BeginCreateCredentialResponse? readFromBundle(android.os.Bundle bundle);
+ method public android.os.Bundle writeToBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
+ }
+
+ public class BeginCreateCustomCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
+ ctor public BeginCreateCustomCredentialRequest(String type, android.os.Bundle candidateQueryData, android.service.credentials.CallingAppInfo? callingAppInfo);
+ }
+
+ public final class BeginCreatePasswordCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
+ ctor public BeginCreatePasswordCredentialRequest(android.service.credentials.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
+ }
+
+ public final class BeginCreatePublicKeyCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
+ ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, android.service.credentials.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);
+ ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, android.service.credentials.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
+ 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, optional android.service.credentials.CallingAppInfo? callingAppInfo);
+ ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions);
+ method public java.util.List<androidx.credentials.provider.BeginGetCredentialOption> getBeginGetCredentialOptions();
+ method public android.service.credentials.CallingAppInfo? getCallingAppInfo();
+ method public static androidx.credentials.provider.BeginGetCredentialRequest? readFromBundle(android.os.Bundle bundle);
+ method public static android.os.Bundle writeToBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
+ property public final java.util.List<androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions;
+ property public final android.service.credentials.CallingAppInfo? callingAppInfo;
+ field public static final androidx.credentials.provider.BeginGetCredentialRequest.Companion Companion;
+ }
+
+ public static final class BeginGetCredentialRequest.Companion {
+ method public androidx.credentials.provider.BeginGetCredentialRequest? readFromBundle(android.os.Bundle bundle);
+ method public android.os.Bundle writeToBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
+ }
+
+ 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 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();
+ method public static androidx.credentials.provider.BeginGetCredentialResponse? readFromBundle(android.os.Bundle bundle);
+ method public static android.os.Bundle writeToBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
+ 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 androidx.credentials.provider.BeginGetCredentialResponse? readFromBundle(android.os.Bundle bundle);
+ method public android.os.Bundle writeToBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
+ }
+
+ 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 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, optional byte[]? clientDataHash);
+ ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson);
+ method public byte[]? getClientDataHash();
+ method public String getRequestJson();
+ property public final byte[]? clientDataHash;
+ property public final String requestJson;
+ }
+
+ 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);
+ 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();
+ property public final CharSequence accountName;
+ property public final CharSequence? description;
+ property public final android.graphics.drawable.Icon? icon;
+ property public final java.time.Instant? lastUsedTime;
+ property public final android.app.PendingIntent pendingIntent;
+ }
+
+ 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 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 abstract class CredentialEntry {
+ method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
+ property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
+ }
+
+ @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(28) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+ 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);
+ 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 isAutoSelectAllowed();
+ 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;
+ property public final CharSequence? subtitle;
+ property public final CharSequence title;
+ property public String type;
+ property public final CharSequence? typeDisplayName;
+ }
+
+ 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 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);
+ }
+
+ @RequiresApi(28) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+ 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);
+ 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 isAutoSelectAllowed();
+ property public final CharSequence? displayName;
+ 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;
+ property public final CharSequence typeDisplayName;
+ property public final CharSequence username;
+ }
+
+ 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 setAutoSelectAllowed(boolean autoSelectAllowed);
+ 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);
+ }
+
+ @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(android.service.credentials.CallingAppInfo callingAppInfo);
+ method public android.service.credentials.CallingAppInfo getCallingAppInfo();
+ property public final android.service.credentials.CallingAppInfo callingAppInfo;
+ }
+
+ public final class ProviderCreateCredentialRequest {
+ ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, android.service.credentials.CallingAppInfo callingAppInfo);
+ method public android.service.credentials.CallingAppInfo getCallingAppInfo();
+ method public androidx.credentials.CreateCredentialRequest getCallingRequest();
+ property public final android.service.credentials.CallingAppInfo callingAppInfo;
+ property public final androidx.credentials.CreateCredentialRequest callingRequest;
+ }
+
+ @RequiresApi(34) public final class ProviderGetCredentialRequest {
+ ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, android.service.credentials.CallingAppInfo callingAppInfo);
+ method public android.service.credentials.CallingAppInfo getCallingAppInfo();
+ method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
+ property public final android.service.credentials.CallingAppInfo callingAppInfo;
+ property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
+ }
+
+ @RequiresApi(28) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+ 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);
+ 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 isAutoSelectAllowed();
+ property public final CharSequence? displayName;
+ 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;
+ property public final CharSequence typeDisplayName;
+ property public final CharSequence username;
+ }
+
+ 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 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 final class RemoteEntry {
+ ctor public RemoteEntry(android.app.PendingIntent pendingIntent);
+ method public android.app.PendingIntent getPendingIntent();
+ property public final android.app.PendingIntent pendingIntent;
+ }
+
+}
+
diff --git a/credentials/credentials/api/restricted_current.txt b/credentials/credentials/api/restricted_current.txt
index d15ea9a..cd87117 100644
--- a/credentials/credentials/api/restricted_current.txt
+++ b/credentials/credentials/api/restricted_current.txt
@@ -6,13 +6,28 @@
}
public abstract class CreateCredentialRequest {
+ 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 boolean getPreferImmediatelyAvailableCredentials();
+ method public final String getType();
+ method public final boolean isAutoSelectAllowed();
+ method public final boolean isSystemProviderRequired();
+ 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;
}
public static final class CreateCredentialRequest.DisplayInfo {
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
+ ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
method public CharSequence? getUserDisplayName();
method public CharSequence getUserId();
property public final CharSequence? userDisplayName;
@@ -20,35 +35,28 @@
}
public abstract class CreateCredentialResponse {
- }
-
- 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, 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);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
- method public final android.os.Bundle getCandidateQueryData();
- method public final android.os.Bundle getCredentialData();
- method public final String getType();
- method public final boolean isAutoSelectAllowed();
- method public final boolean isSystemProviderRequired();
- property public final android.os.Bundle candidateQueryData;
- property public final android.os.Bundle credentialData;
- property public final boolean isAutoSelectAllowed;
- property public final boolean isSystemProviderRequired;
- property public final String type;
- }
-
- public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);
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, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
+ 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);
+ ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
+ }
+
+ 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, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
ctor public CreatePasswordRequest(String id, String password, optional String? origin);
ctor public CreatePasswordRequest(String id, String password);
+ ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials);
method public String getId();
method public String getPassword();
property public final String id;
@@ -60,15 +68,14 @@
}
public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash);
+ 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);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);
ctor public CreatePublicKeyCredentialRequest(String requestJson);
- method public String? getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, byte[]? clientDataHash, boolean preferImmediatelyAvailableCredentials, String? origin, String? preferDefaultProvider);
+ method public byte[]? getClientDataHash();
method public String getRequestJson();
- property public final String? clientDataHash;
- property public final boolean preferImmediatelyAvailableCredentials;
+ property public final byte[]? clientDataHash;
property public final String requestJson;
}
@@ -79,16 +86,25 @@
}
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 final class CredentialManager {
- method public suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ @RequiresApi(16) 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 suspend Object? createCredential(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
- method public void createCredentialAsync(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method public suspend Object? getCredential(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
- method public void getCredentialAsync(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
+ method public default 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;
}
@@ -102,30 +118,49 @@
}
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 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;
}
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(androidx.credentials.CreateCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
- method public void onGetCredential(androidx.credentials.GetCredentialRequest request, android.app.Activity activity, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> 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);
- 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 final class GetCredentialRequest {
+ 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);
+ 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);
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);
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
method public String? getOrigin();
+ method public boolean getPreferIdentityDocUi();
+ method public boolean getPreferImmediatelyAvailableCredentials();
+ method public android.content.ComponentName? getPreferUiBrandingComponentName();
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 {
@@ -134,6 +169,9 @@
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 {
@@ -143,36 +181,27 @@
}
public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
+ 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);
ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
- method public final android.os.Bundle getCandidateQueryData();
- method public final android.os.Bundle getRequestData();
- method public final String getType();
- method public final boolean isAutoSelectAllowed();
- method public final boolean isSystemProviderRequired();
- 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;
}
public final class GetPasswordOption extends androidx.credentials.CredentialOption {
- ctor public GetPasswordOption(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);
+ 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);
ctor public GetPasswordOption();
- method public boolean isAutoSelectAllowed();
- property public boolean isAutoSelectAllowed;
+ 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, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional String? clientDataHash);
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set<android.content.ComponentName> allowedProviders);
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);
ctor public GetPublicKeyCredentialOption(String requestJson);
- method public String? getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
+ method public byte[]? getClientDataHash();
method public String getRequestJson();
- property public final String? clientDataHash;
- property public final boolean preferImmediatelyAvailableCredentials;
+ property public final byte[]? clientDataHash;
property public final String requestJson;
}
@@ -182,12 +211,34 @@
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 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();
+ 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);
}
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 {
}
}
@@ -454,3 +505,346 @@
}
+package androidx.credentials.provider {
+
+ public final class Action {
+ ctor public Action(CharSequence title, android.app.PendingIntent pendingIntent, optional CharSequence? subtitle);
+ 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;
+ }
+
+ 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 final class AuthenticationAction {
+ ctor public AuthenticationAction(CharSequence title, android.app.PendingIntent pendingIntent);
+ method public android.app.PendingIntent getPendingIntent();
+ method public CharSequence getTitle();
+ property public final android.app.PendingIntent pendingIntent;
+ property public final CharSequence title;
+ }
+
+ public abstract class BeginCreateCredentialRequest {
+ ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, android.service.credentials.CallingAppInfo? callingAppInfo);
+ method public final android.service.credentials.CallingAppInfo? getCallingAppInfo();
+ method public final android.os.Bundle getCandidateQueryData();
+ method public final String getType();
+ method public static final androidx.credentials.provider.BeginCreateCredentialRequest? readFromBundle(android.os.Bundle bundle);
+ method public static final android.os.Bundle writeToBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
+ property public final android.service.credentials.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 androidx.credentials.provider.BeginCreateCredentialRequest? readFromBundle(android.os.Bundle bundle);
+ method public android.os.Bundle writeToBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
+ }
+
+ public final class BeginCreateCredentialResponse {
+ ctor public BeginCreateCredentialResponse(optional java.util.List<androidx.credentials.provider.CreateEntry> createEntries, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
+ method public java.util.List<androidx.credentials.provider.CreateEntry> getCreateEntries();
+ method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
+ method public static androidx.credentials.provider.BeginCreateCredentialResponse? readFromBundle(android.os.Bundle bundle);
+ method public static android.os.Bundle writeToBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
+ 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 androidx.credentials.provider.BeginCreateCredentialResponse? readFromBundle(android.os.Bundle bundle);
+ method public android.os.Bundle writeToBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
+ }
+
+ public class BeginCreateCustomCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
+ ctor public BeginCreateCustomCredentialRequest(String type, android.os.Bundle candidateQueryData, android.service.credentials.CallingAppInfo? callingAppInfo);
+ }
+
+ public final class BeginCreatePasswordCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
+ ctor public BeginCreatePasswordCredentialRequest(android.service.credentials.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
+ }
+
+ public final class BeginCreatePublicKeyCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
+ ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, android.service.credentials.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);
+ ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, android.service.credentials.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
+ 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, optional android.service.credentials.CallingAppInfo? callingAppInfo);
+ ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions);
+ method public java.util.List<androidx.credentials.provider.BeginGetCredentialOption> getBeginGetCredentialOptions();
+ method public android.service.credentials.CallingAppInfo? getCallingAppInfo();
+ method public static androidx.credentials.provider.BeginGetCredentialRequest? readFromBundle(android.os.Bundle bundle);
+ method public static android.os.Bundle writeToBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
+ property public final java.util.List<androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions;
+ property public final android.service.credentials.CallingAppInfo? callingAppInfo;
+ field public static final androidx.credentials.provider.BeginGetCredentialRequest.Companion Companion;
+ }
+
+ public static final class BeginGetCredentialRequest.Companion {
+ method public androidx.credentials.provider.BeginGetCredentialRequest? readFromBundle(android.os.Bundle bundle);
+ method public android.os.Bundle writeToBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
+ }
+
+ 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 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();
+ method public static androidx.credentials.provider.BeginGetCredentialResponse? readFromBundle(android.os.Bundle bundle);
+ method public static android.os.Bundle writeToBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
+ 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 androidx.credentials.provider.BeginGetCredentialResponse? readFromBundle(android.os.Bundle bundle);
+ method public android.os.Bundle writeToBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
+ }
+
+ 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 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, optional byte[]? clientDataHash);
+ ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson);
+ method public byte[]? getClientDataHash();
+ method public String getRequestJson();
+ property public final byte[]? clientDataHash;
+ property public final String requestJson;
+ }
+
+ 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);
+ 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();
+ property public final CharSequence accountName;
+ property public final CharSequence? description;
+ property public final android.graphics.drawable.Icon? icon;
+ property public final java.time.Instant? lastUsedTime;
+ property public final android.app.PendingIntent pendingIntent;
+ }
+
+ 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 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 abstract class CredentialEntry {
+ method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
+ property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
+ }
+
+ @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(28) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+ 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);
+ 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 isAutoSelectAllowed();
+ 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;
+ property public final CharSequence? subtitle;
+ property public final CharSequence title;
+ property public String type;
+ property public final CharSequence? typeDisplayName;
+ }
+
+ 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 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);
+ }
+
+ @RequiresApi(28) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+ 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);
+ 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 isAutoSelectAllowed();
+ property public final CharSequence? displayName;
+ 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;
+ property public final CharSequence typeDisplayName;
+ property public final CharSequence username;
+ }
+
+ 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 setAutoSelectAllowed(boolean autoSelectAllowed);
+ 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);
+ }
+
+ @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(android.service.credentials.CallingAppInfo callingAppInfo);
+ method public android.service.credentials.CallingAppInfo getCallingAppInfo();
+ property public final android.service.credentials.CallingAppInfo callingAppInfo;
+ }
+
+ public final class ProviderCreateCredentialRequest {
+ ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, android.service.credentials.CallingAppInfo callingAppInfo);
+ method public android.service.credentials.CallingAppInfo getCallingAppInfo();
+ method public androidx.credentials.CreateCredentialRequest getCallingRequest();
+ property public final android.service.credentials.CallingAppInfo callingAppInfo;
+ property public final androidx.credentials.CreateCredentialRequest callingRequest;
+ }
+
+ @RequiresApi(34) public final class ProviderGetCredentialRequest {
+ ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, android.service.credentials.CallingAppInfo callingAppInfo);
+ method public android.service.credentials.CallingAppInfo getCallingAppInfo();
+ method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
+ property public final android.service.credentials.CallingAppInfo callingAppInfo;
+ property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
+ }
+
+ @RequiresApi(28) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+ 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);
+ 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 isAutoSelectAllowed();
+ property public final CharSequence? displayName;
+ 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;
+ property public final CharSequence typeDisplayName;
+ property public final CharSequence username;
+ }
+
+ 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 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 final class RemoteEntry {
+ ctor public RemoteEntry(android.app.PendingIntent pendingIntent);
+ method public android.app.PendingIntent getPendingIntent();
+ property public final android.app.PendingIntent pendingIntent;
+ }
+
+}
+
diff --git a/credentials/credentials/build.gradle b/credentials/credentials/build.gradle
index 0c8c3fb..0a9bae2 100644
--- a/credentials/credentials/build.gradle
+++ b/credentials/credentials/build.gradle
@@ -26,6 +26,7 @@
api("androidx.annotation:annotation:1.5.0")
api(libs.kotlinStdlib)
implementation(libs.kotlinCoroutinesCore)
+ api(project(":core:core"))
androidTestImplementation("androidx.activity:activity:1.2.0")
androidTestImplementation(libs.junit)
@@ -36,6 +37,9 @@
androidTestImplementation(libs.truth)
androidTestImplementation(project(":internal-testutils-truth"))
androidTestImplementation(libs.kotlinCoroutinesAndroid)
+ androidTestImplementation(project(":internal-testutils-runtime"), {
+ exclude group: "androidx.fragment", module: "fragment"
+ })
}
android {
@@ -47,7 +51,7 @@
}
androidx {
- name = "Credentials Core Library"
+ name = "Credentials"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2022"
description = "Android Credentials Library"
diff --git a/credentials/credentials/lint-baseline.xml b/credentials/credentials/lint-baseline.xml
index f43f80f..8745e71 100644
--- a/credentials/credentials/lint-baseline.xml
+++ b/credentials/credentials/lint-baseline.xml
@@ -13,6 +13,384 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
+ errorLine1=" companion object {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun toSlice("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun toSlice("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun fromSlice(slice: Slice): Action? {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun fromSlice(slice: Slice): Action? {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" companion object {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun toSlice(authenticationAction: AuthenticationAction): Slice {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun toSlice(authenticationAction: AuthenticationAction): Slice {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun fromSlice(slice: Slice): AuthenticationAction? {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun fromSlice(slice: Slice): AuthenticationAction? {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1="class BeginCreateCredentialUtil {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/utils/BeginCreateCredentialUtil.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" companion object {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginCreatePasswordCredentialRequest.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun createFrom(data: Bundle, callingAppInfo: CallingAppInfo?):"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginCreatePasswordCredentialRequest.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun createFrom(data: Bundle, callingAppInfo: CallingAppInfo?):"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginCreatePasswordCredentialRequest.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" companion object {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginCreatePublicKeyCredentialRequest.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun createFrom(data: Bundle, callingAppInfo: CallingAppInfo?):"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginCreatePublicKeyCredentialRequest.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun createFrom(data: Bundle, callingAppInfo: CallingAppInfo?):"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginCreatePublicKeyCredentialRequest.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" open val id: String,"
+ errorLine2=" ~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" open val id: String,"
+ errorLine2=" ~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" open val id: String,"
+ errorLine2=" ~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" open val type: String,"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" open val type: String,"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" open val type: String,"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" open val candidateQueryData: Bundle"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" open val candidateQueryData: Bundle"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" open val candidateQueryData: Bundle"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" companion object {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" companion object {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetCustomCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun createFrom("
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetCustomCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun createFrom("
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetCustomCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun createFromEntrySlice("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetCustomCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun createFromEntrySlice("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetCustomCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" companion object {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetPasswordOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun createFrom(data: Bundle, id: String): BeginGetPasswordOption {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetPasswordOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun createFrom(data: Bundle, id: String): BeginGetPasswordOption {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetPasswordOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun createFromEntrySlice(data: Bundle, id: String): BeginGetPasswordOption {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetPasswordOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun createFromEntrySlice(data: Bundle, id: String): BeginGetPasswordOption {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetPasswordOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" companion object {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun createFrom(data: Bundle, id: String): BeginGetPublicKeyCredentialOption {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun createFrom(data: Bundle, id: String): BeginGetPublicKeyCredentialOption {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun createFromEntrySlice(data: Bundle, id: String):"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun createFromEntrySlice(data: Bundle, id: String):"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOption.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
errorLine1=" open val type: String,"
errorLine2=" ~~~~">
<location
@@ -220,168 +598,6 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1=" open val type: String,"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val type: String,"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val type: String,"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val credentialData: Bundle,"
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val credentialData: Bundle,"
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val credentialData: Bundle,"
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val candidateQueryData: Bundle,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val candidateQueryData: Bundle,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val candidateQueryData: Bundle,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val isSystemProviderRequired: Boolean,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val isSystemProviderRequired: Boolean,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val isSystemProviderRequired: Boolean,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val isAutoSelectAllowed: Boolean,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val isAutoSelectAllowed: Boolean,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val isAutoSelectAllowed: Boolean,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" val displayInfo: DisplayInfo,"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" val displayInfo: DisplayInfo,"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" val displayInfo: DisplayInfo,"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
errorLine1=" class DisplayInfo internal /** @hide */ constructor("
errorLine2=" ~~~~~~~~~~~">
<location
@@ -418,8 +634,8 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1=" val defaultProvider: String?,"
- errorLine2=" ~~~~~~~~~~~~~~~">
+ errorLine1=" val preferDefaultProvider: String?,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
</issue>
@@ -427,8 +643,8 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1=" val defaultProvider: String?,"
- errorLine2=" ~~~~~~~~~~~~~~~">
+ errorLine1=" val preferDefaultProvider: String?,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
</issue>
@@ -436,8 +652,8 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1=" val defaultProvider: String?,"
- errorLine2=" ~~~~~~~~~~~~~~~">
+ errorLine1=" val preferDefaultProvider: String?,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/credentials/CreateCredentialRequest.kt"/>
</issue>
@@ -544,60 +760,6 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1=" open val type: String,"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialResponse.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val type: String,"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialResponse.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val type: String,"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialResponse.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val data: Bundle,"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialResponse.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val data: Bundle,"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialResponse.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val data: Bundle,"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/credentials/CreateCredentialResponse.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
errorLine1=" companion object {"
errorLine2=" ~~~~~~">
<location
@@ -646,6 +808,87 @@
errorLine1=" companion object {"
errorLine2=" ~~~~~~">
<location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun toSlice("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun toSlice("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun fromSlice(slice: Slice): CreateEntry? {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun fromSlice(slice: Slice): CreateEntry? {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun convertBundleToCredentialCountInfo(bundle: Bundle?):"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun convertBundleToCredentialCountInfo(bundle: Bundle?):"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun convertCredentialCountInfoToBundle("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" internal fun convertCredentialCountInfoToBundle("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" companion object {"
+ errorLine2=" ~~~~~~">
+ <location
file="src/main/java/androidx/credentials/CreatePasswordRequest.kt"/>
</issue>
@@ -724,60 +967,6 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1=" open val type: String,"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/credentials/Credential.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val type: String,"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/credentials/Credential.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val type: String,"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/credentials/Credential.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val data: Bundle,"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/credentials/Credential.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val data: Bundle,"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/credentials/Credential.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val data: Bundle,"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/credentials/Credential.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
errorLine1=" companion object {"
errorLine2=" ~~~~~~">
<location
@@ -808,7 +997,7 @@
errorLine1=" open val type: String,"
errorLine2=" ~~~~">
<location
- file="src/main/java/androidx/credentials/CredentialOption.kt"/>
+ file="src/main/java/androidx/credentials/provider/CredentialEntry.kt"/>
</issue>
<issue
@@ -817,7 +1006,7 @@
errorLine1=" open val type: String,"
errorLine2=" ~~~~">
<location
- file="src/main/java/androidx/credentials/CredentialOption.kt"/>
+ file="src/main/java/androidx/credentials/provider/CredentialEntry.kt"/>
</issue>
<issue
@@ -826,115 +1015,43 @@
errorLine1=" open val type: String,"
errorLine2=" ~~~~">
<location
- file="src/main/java/androidx/credentials/CredentialOption.kt"/>
+ file="src/main/java/androidx/credentials/provider/CredentialEntry.kt"/>
</issue>
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1=" open val requestData: Bundle,"
- errorLine2=" ~~~~~~~~~~~">
+ errorLine1=" val slice: Slice"
+ errorLine2=" ~~~~~">
<location
- file="src/main/java/androidx/credentials/CredentialOption.kt"/>
+ file="src/main/java/androidx/credentials/provider/CredentialEntry.kt"/>
</issue>
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1=" open val requestData: Bundle,"
- errorLine2=" ~~~~~~~~~~~">
+ errorLine1=" val slice: Slice"
+ errorLine2=" ~~~~~">
<location
- file="src/main/java/androidx/credentials/CredentialOption.kt"/>
+ file="src/main/java/androidx/credentials/provider/CredentialEntry.kt"/>
</issue>
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1=" open val requestData: Bundle,"
- errorLine2=" ~~~~~~~~~~~">
+ errorLine1=" val slice: Slice"
+ errorLine2=" ~~~~~">
<location
- file="src/main/java/androidx/credentials/CredentialOption.kt"/>
+ file="src/main/java/androidx/credentials/provider/CredentialEntry.kt"/>
</issue>
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1=" open val candidateQueryData: Bundle,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ errorLine1=" companion object {"
+ errorLine2=" ~~~~~~">
<location
- file="src/main/java/androidx/credentials/CredentialOption.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val candidateQueryData: Bundle,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CredentialOption.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val candidateQueryData: Bundle,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CredentialOption.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val isSystemProviderRequired: Boolean,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CredentialOption.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val isSystemProviderRequired: Boolean,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CredentialOption.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val isSystemProviderRequired: Boolean,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CredentialOption.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val isAutoSelectAllowed: Boolean,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CredentialOption.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val isAutoSelectAllowed: Boolean,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CredentialOption.kt"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1=" open val isAutoSelectAllowed: Boolean,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/credentials/CredentialOption.kt"/>
+ file="src/main/java/androidx/credentials/provider/CredentialEntry.kt"/>
</issue>
<issue
@@ -985,6 +1102,123 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
+ errorLine1="class CredentialProviderFrameworkImpl(context: Context) : CredentialProvider {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/CredentialProviderFrameworkImpl.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" companion object {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/CredentialProviderFrameworkImpl.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" val autoSelectAllowedFromOption: Boolean = false,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" val autoSelectAllowedFromOption: Boolean = false,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" val autoSelectAllowedFromOption: Boolean = false,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" val isDefaultIcon: Boolean = false"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" val isDefaultIcon: Boolean = false"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" val isDefaultIcon: Boolean = false"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" companion object {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun toSlice("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun toSlice("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun fromSlice(slice: Slice): CustomCredentialEntry? {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun fromSlice(slice: Slice): CustomCredentialEntry? {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
errorLine1=" companion object {"
errorLine2=" ~~~~~~">
<location
@@ -1177,6 +1411,51 @@
errorLine1=" companion object {"
errorLine2=" ~~~~~~">
<location
+ file="src/main/java/androidx/credentials/GetCredentialRequest.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun toRequestDataBundle("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/GetCredentialRequest.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun toRequestDataBundle("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/GetCredentialRequest.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun createFrom("
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/GetCredentialRequest.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun createFrom("
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/GetCredentialRequest.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" companion object {"
+ errorLine2=" ~~~~~~">
+ <location
file="src/main/java/androidx/credentials/exceptions/GetCredentialUnknownException.kt"/>
</issue>
@@ -1399,19 +1678,55 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1=" companion object {"
- errorLine2=" ~~~~~~">
+ errorLine1=" val autoSelectAllowedFromOption: Boolean = false,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
- file="src/main/java/androidx/credentials/PasswordCredential.kt"/>
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
</issue>
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1=" const val TYPE_PASSWORD_CREDENTIAL: String = "android.credentials.TYPE_PASSWORD_CREDENTIAL""
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ errorLine1=" val autoSelectAllowedFromOption: Boolean = false,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
- file="src/main/java/androidx/credentials/PasswordCredential.kt"/>
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" val autoSelectAllowedFromOption: Boolean = false,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" val isDefaultIcon: Boolean = false"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" val isDefaultIcon: Boolean = false"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" val isDefaultIcon: Boolean = false"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
</issue>
<issue
@@ -1420,16 +1735,151 @@
errorLine1=" companion object {"
errorLine2=" ~~~~~~">
<location
- file="src/main/java/androidx/credentials/PublicKeyCredential.kt"/>
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
</issue>
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1=" const val TYPE_PUBLIC_KEY_CREDENTIAL: String ="
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ errorLine1=" fun toSlice("
+ errorLine2=" ~~~~~~~">
<location
- file="src/main/java/androidx/credentials/PublicKeyCredential.kt"/>
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun toSlice("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun fromSlice(slice: Slice): PasswordCredentialEntry? {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun fromSlice(slice: Slice): PasswordCredentialEntry? {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" companion object {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" val autoSelectAllowedFromOption: Boolean = false,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" val autoSelectAllowedFromOption: Boolean = false,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" val autoSelectAllowedFromOption: Boolean = false,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" val isDefaultIcon: Boolean = false"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" val isDefaultIcon: Boolean = false"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" val isDefaultIcon: Boolean = false"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" companion object {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun toSlice("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun toSlice("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun fromSlice(slice: Slice): PublicKeyCredentialEntry? {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun fromSlice(slice: Slice): PublicKeyCredentialEntry? {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
</issue>
<issue
@@ -1456,6 +1906,51 @@
errorLine1=" companion object {"
errorLine2=" ~~~~~~">
<location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun toSlice("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun toSlice("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun fromSlice(slice: Slice): RemoteEntry? {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" fun fromSlice(slice: Slice): RemoteEntry? {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" companion object {"
+ errorLine2=" ~~~~~~">
+ <location
file="src/main/java/androidx/credentials/exceptions/domerrors/SecurityError.kt"/>
</issue>
@@ -1514,6 +2009,1230 @@
</issue>
<issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" val sliceBuilder = Slice.Builder("
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" val sliceBuilder = Slice.Builder("
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" Uri.EMPTY, SliceSpec("
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" Uri.EMPTY, SliceSpec("
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .addText("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .addText("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .addText("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .addText("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" sliceBuilder.addAction("
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" sliceBuilder.addAction("
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" Slice.Builder(sliceBuilder)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" Slice.Builder(sliceBuilder)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .build(),"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .build(),"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" return sliceBuilder.build()"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" return sliceBuilder.build()"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" slice.items.forEach {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" slice.items.forEach {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" if (it.hasHint(SLICE_HINT_TITLE)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" if (it.hasHint(SLICE_HINT_TITLE)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" title = it.text"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" title = it.text"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" } else if (it.hasHint(SLICE_HINT_SUBTITLE)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" } else if (it.hasHint(SLICE_HINT_SUBTITLE)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" subtitle = it.text"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" subtitle = it.text"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" pendingIntent = it.action"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.Action.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" pendingIntent = it.action"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" val sliceBuilder = Slice.Builder("
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" val sliceBuilder = Slice.Builder("
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" Uri.EMPTY, SliceSpec("
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" Uri.EMPTY, SliceSpec("
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .addAction("
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .addAction("
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" Slice.Builder(sliceBuilder)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" Slice.Builder(sliceBuilder)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .build(),"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .build(),"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .addText(title, /*subType=*/null, listOf(SLICE_HINT_TITLE))"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .addText(title, /*subType=*/null, listOf(SLICE_HINT_TITLE))"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" return sliceBuilder.build()"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" return sliceBuilder.build()"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" slice.items.forEach {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" slice.items.forEach {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" pendingIntent = it.action"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" pendingIntent = it.action"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" } else if (it.hasHint(SLICE_HINT_TITLE)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" } else if (it.hasHint(SLICE_HINT_TITLE)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" title = it.text"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.AuthenticationAction.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" title = it.text"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" sliceBuilder.addText("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" sliceBuilder.addText("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" sliceBuilder.addLong("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" sliceBuilder.addLong("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 26; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" lastUsedTime.toEpochMilli(), /*subType=*/null, listOf("
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 26; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" lastUsedTime.toEpochMilli(), /*subType=*/null, listOf("
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" sliceBuilder.addText("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" sliceBuilder.addText("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" sliceBuilder.addIcon("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" sliceBuilder.addIcon("
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" sliceBuilder.addBundle("
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" sliceBuilder.addBundle("
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" sliceBuilder.addAction("
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" sliceBuilder.addAction("
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" Slice.Builder(sliceBuilder)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" Slice.Builder(sliceBuilder)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .build(),"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .build(),"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" return sliceBuilder.build()"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" return sliceBuilder.build()"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" slice.items.forEach {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" slice.items.forEach {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" if (it.hasHint(SLICE_HINT_ACCOUNT_NAME)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" if (it.hasHint(SLICE_HINT_ACCOUNT_NAME)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" accountName = it.text"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" accountName = it.text"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" } else if (it.hasHint(SLICE_HINT_ICON)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" } else if (it.hasHint(SLICE_HINT_ICON)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" icon = it.icon"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" icon = it.icon"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" pendingIntent = it.action"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" pendingIntent = it.action"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" } else if (it.hasHint(SLICE_HINT_CREDENTIAL_COUNT_INFORMATION)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" } else if (it.hasHint(SLICE_HINT_CREDENTIAL_COUNT_INFORMATION)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" credentialCountInfo = convertBundleToCredentialCountInfo(it.bundle)"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" credentialCountInfo = convertBundleToCredentialCountInfo(it.bundle)"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" } else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" } else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 26; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" lastUsedTime = Instant.ofEpochMilli(it.long)"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 26; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" lastUsedTime = Instant.ofEpochMilli(it.long)"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" lastUsedTime = Instant.ofEpochMilli(it.long)"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" lastUsedTime = Instant.ofEpochMilli(it.long)"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" } else if (it.hasHint(SLICE_HINT_NOTE)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" } else if (it.hasHint(SLICE_HINT_NOTE)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" description = it.text"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CreateEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" description = it.text"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CredentialEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" when (slice.spec?.type) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CredentialEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" when (slice.spec?.type) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CredentialEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" when (slice.spec?.type) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.CredentialEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" when (slice.spec?.type) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" sliceBuilder.addAction("
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" sliceBuilder.addAction("
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" Slice.Builder(sliceBuilder)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" Slice.Builder(sliceBuilder)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .build(), /*subType=*/null"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" .build(), /*subType=*/null"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" return sliceBuilder.build()"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" return sliceBuilder.build()"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" slice.items.forEach {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" slice.items.forEach {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" pendingIntent = it.action"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="ClassVerificationFailure"
+ message="This call references a method added in API level 28; however, the containing class androidx.credentials.provider.RemoteEntry.Companion is reachable from earlier API levels and will fail run-time class verification."
+ errorLine1=" pendingIntent = it.action"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
id="UsesNonDefaultVisibleForTesting"
message="Found non-default `otherwise` value for @VisibleForTesting"
errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)"
@@ -1525,6 +3244,51 @@
<issue
id="UsesNonDefaultVisibleForTesting"
message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/Action.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/AuthenticationAction.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -1564,6 +3328,69 @@
errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CreateEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
file="src/main/java/androidx/credentials/CreatePasswordRequest.kt"/>
</issue>
@@ -1597,6 +3424,114 @@
<issue
id="UsesNonDefaultVisibleForTesting"
message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -1780,7 +3715,7 @@
errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
- file="src/main/java/androidx/credentials/PasswordCredential.kt"/>
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
</issue>
<issue
@@ -1789,7 +3724,7 @@
errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
- file="src/main/java/androidx/credentials/PasswordCredential.kt"/>
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
</issue>
<issue
@@ -1798,7 +3733,196 @@
errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
- file="src/main/java/androidx/credentials/PublicKeyCredential.kt"/>
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt"/>
</issue>
<issue
@@ -1822,6 +3946,15 @@
<issue
id="UsesNonDefaultVisibleForTesting"
message="Found non-default `otherwise` value for @VisibleForTesting"
+ errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/credentials/provider/RemoteEntry.kt"/>
+ </issue>
+
+ <issue
+ id="UsesNonDefaultVisibleForTesting"
+ message="Found non-default `otherwise` value for @VisibleForTesting"
errorLine1=" @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java
index 40acdb0..62ef10f 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java
@@ -78,7 +78,24 @@
assertThat(displayInfo.getUserId()).isEqualTo(expectedUserId);
assertThat(displayInfo.getUserDisplayName()).isEqualTo(expectedDisplayName);
assertThat(displayInfo.getCredentialTypeIcon()).isNull();
- assertThat(displayInfo.getDefaultProvider()).isNull();
+ assertThat(displayInfo.getPreferDefaultProvider()).isNull();
+ }
+
+ @SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+ @Test
+ public void constructWithUserIdAndDisplayNameAndDefaultProvider_success() {
+ CharSequence expectedUserId = "userId";
+ CharSequence expectedDisplayName = "displayName";
+ String expectedDefaultProvider = "com.test/com.test.TestProviderComponent";
+
+ CreateCredentialRequest.DisplayInfo displayInfo =
+ new CreateCredentialRequest.DisplayInfo(expectedUserId,
+ expectedDisplayName, expectedDefaultProvider);
+
+ assertThat(displayInfo.getUserId()).isEqualTo(expectedUserId);
+ assertThat(displayInfo.getUserDisplayName()).isEqualTo(expectedDisplayName);
+ assertThat(displayInfo.getCredentialTypeIcon()).isNull();
+ assertThat(displayInfo.getPreferDefaultProvider()).isEqualTo(expectedDefaultProvider);
}
@SdkSuppress(minSdkVersion = 28)
@@ -96,7 +113,7 @@
assertThat(displayInfo.getUserId()).isEqualTo(expectedUserId);
assertThat(displayInfo.getUserDisplayName()).isEqualTo(expectedDisplayName);
assertThat(displayInfo.getCredentialTypeIcon()).isEqualTo(expectedIcon);
- assertThat(displayInfo.getDefaultProvider()).isEqualTo(expectedDefaultProvider);
+ assertThat(displayInfo.getPreferDefaultProvider()).isEqualTo(expectedDefaultProvider);
}
@SdkSuppress(minSdkVersion = 28)
@@ -115,6 +132,6 @@
assertThat(displayInfo.getUserDisplayName()).isNull();
assertThat(displayInfo.getCredentialTypeIcon().getResId()).isEqualTo(
R.drawable.ic_password);
- assertThat(displayInfo.getDefaultProvider()).isNull();
+ assertThat(displayInfo.getPreferDefaultProvider()).isNull();
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt
index bfde3e9..a495264 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt
@@ -66,7 +66,26 @@
assertThat(displayInfo.userId).isEqualTo(expectedUserId)
assertThat(displayInfo.userDisplayName).isEqualTo(expectedDisplayName)
assertThat(displayInfo.credentialTypeIcon).isNull()
- assertThat(displayInfo.defaultProvider).isNull()
+ assertThat(displayInfo.preferDefaultProvider).isNull()
+ }
+
+ @SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+ @Test
+ fun constructWithUserIdAndDisplayNameAndDefaultProvider_success() {
+ val expectedUserId: CharSequence = "userId"
+ val expectedDisplayName: CharSequence = "displayName"
+ val expectedDefaultProvider = "com.test/com.test.TestProviderComponent"
+
+ val displayInfo = DisplayInfo(
+ userId = expectedUserId,
+ userDisplayName = expectedDisplayName,
+ preferDefaultProvider = expectedDefaultProvider
+ )
+
+ assertThat(displayInfo.userId).isEqualTo(expectedUserId)
+ assertThat(displayInfo.userDisplayName).isEqualTo(expectedDisplayName)
+ assertThat(displayInfo.credentialTypeIcon).isNull()
+ assertThat(displayInfo.preferDefaultProvider).isEqualTo(expectedDefaultProvider)
}
@SdkSuppress(minSdkVersion = 28)
@@ -85,7 +104,7 @@
assertThat(displayInfo.userId).isEqualTo(expectedUserId)
assertThat(displayInfo.userDisplayName).isEqualTo(expectedDisplayName)
assertThat(displayInfo.credentialTypeIcon).isEqualTo(expectedIcon)
- assertThat(displayInfo.defaultProvider).isEqualTo(expectedDefaultProvider)
+ assertThat(displayInfo.preferDefaultProvider).isEqualTo(expectedDefaultProvider)
}
@SdkSuppress(minSdkVersion = 28)
@@ -105,6 +124,6 @@
assertThat(displayInfo.credentialTypeIcon?.resId).isEqualTo(
R.drawable.ic_password
)
- assertThat(displayInfo.defaultProvider).isNull()
+ assertThat(displayInfo.preferDefaultProvider).isNull()
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestJavaTest.java
index 893f37b..66ef8ac 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestJavaTest.java
@@ -16,12 +16,17 @@
package androidx.credentials;
+import static androidx.credentials.CreateCredentialRequest.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED;
+import static androidx.credentials.CreateCredentialRequest.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.os.Bundle;
+import androidx.test.filters.SdkSuppress;
+
import org.junit.Test;
public class CreateCustomCredentialRequestJavaTest {
@@ -71,24 +76,37 @@
new CreateCredentialRequest.DisplayInfo("userId"), true);
}
+ @SdkSuppress(minSdkVersion = 26)
@Test
public void getter() {
String expectedType = "TYPE";
- Bundle expectedCredentialDataBundle = new Bundle();
- expectedCredentialDataBundle.putString("Test", "Test");
- Bundle expectedCandidateQueryDataBundle = new Bundle();
- expectedCandidateQueryDataBundle.putBoolean("key", true);
+ boolean expectedAutoSelectAllowed = true;
+ boolean expectedPreferImmediatelyAvailableCredentials = true;
+ Bundle inputCredentialDataBundle = new Bundle();
+ inputCredentialDataBundle.putString("Test", "Test");
+ Bundle expectedCredentialDataBundle = inputCredentialDataBundle.deepCopy();
+ expectedCredentialDataBundle.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
+ expectedAutoSelectAllowed);
+ expectedCredentialDataBundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+ expectedPreferImmediatelyAvailableCredentials);
+ Bundle inputCandidateQueryDataBundle = new Bundle();
+ inputCandidateQueryDataBundle.putBoolean("key", true);
+ Bundle expectedCandidateQueryDataBundle = inputCandidateQueryDataBundle.deepCopy();
+ expectedCandidateQueryDataBundle.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
+ expectedAutoSelectAllowed);
CreateCredentialRequest.DisplayInfo expectedDisplayInfo =
new CreateCredentialRequest.DisplayInfo("userId");
boolean expectedSystemProvider = true;
- boolean expectedAutoSelectAllowed = false;
+ String expectedOrigin = "Origin";
CreateCustomCredentialRequest request = new CreateCustomCredentialRequest(expectedType,
- expectedCredentialDataBundle,
- expectedCandidateQueryDataBundle,
+ inputCredentialDataBundle,
+ inputCandidateQueryDataBundle,
expectedSystemProvider,
expectedDisplayInfo,
- expectedAutoSelectAllowed);
+ expectedAutoSelectAllowed,
+ expectedOrigin,
+ expectedPreferImmediatelyAvailableCredentials);
assertThat(request.getType()).isEqualTo(expectedType);
assertThat(TestUtilsKt.equals(request.getCredentialData(), expectedCredentialDataBundle))
@@ -97,7 +115,10 @@
expectedCandidateQueryDataBundle)).isTrue();
assertThat(request.isSystemProviderRequired()).isEqualTo(expectedSystemProvider);
assertThat(request.isAutoSelectAllowed()).isEqualTo(expectedAutoSelectAllowed);
+ assertThat(request.preferImmediatelyAvailableCredentials()).isEqualTo(
+ expectedPreferImmediatelyAvailableCredentials);
assertThat(request.getDisplayInfo()).isEqualTo(expectedDisplayInfo);
+ assertThat(request.getOrigin()).isEqualTo(expectedOrigin);
}
@Test
@@ -107,4 +128,57 @@
new Bundle(), new Bundle(), false,
/* requestDisplayInfo= */null, false));
}
+
+
+ @SdkSuppress(minSdkVersion = 23)
+ @Test
+ public void frameworkConversion_success() {
+ String expectedType = "TYPE";
+ Bundle expectedCredentialDataBundle = new Bundle();
+ expectedCredentialDataBundle.putString("Test", "Test");
+ Bundle expectedCandidateQueryDataBundle = new Bundle();
+ expectedCandidateQueryDataBundle.putBoolean("key", true);
+ CreateCredentialRequest.DisplayInfo expectedDisplayInfo =
+ new CreateCredentialRequest.DisplayInfo("userId");
+ boolean expectedSystemProvider = true;
+ boolean expectedAutoSelectAllowed = true;
+ boolean expectedPreferImmediatelyAvailableCredentials = true;
+ String expectedOrigin = "Origin";
+ CreateCustomCredentialRequest request = new CreateCustomCredentialRequest(expectedType,
+ expectedCredentialDataBundle,
+ expectedCandidateQueryDataBundle,
+ expectedSystemProvider,
+ expectedDisplayInfo,
+ expectedAutoSelectAllowed,
+ expectedOrigin,
+ expectedPreferImmediatelyAvailableCredentials);
+ Bundle finalCredentialData = request.getCredentialData();
+ finalCredentialData.putBundle(
+ CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_REQUEST_DISPLAY_INFO,
+ expectedDisplayInfo.toBundle()
+ );
+
+ CreateCredentialRequest convertedRequest = CreateCredentialRequest.createFrom(
+ request.getType(), request.getCredentialData(), request.getCandidateQueryData(),
+ request.isSystemProviderRequired(), request.getOrigin());
+
+ assertThat(convertedRequest).isInstanceOf(CreateCustomCredentialRequest.class);
+ CreateCustomCredentialRequest actualRequest =
+ (CreateCustomCredentialRequest) convertedRequest;
+ assertThat(actualRequest.getType()).isEqualTo(expectedType);
+ assertThat(TestUtilsKt.equals(actualRequest.getCredentialData(),
+ expectedCredentialDataBundle))
+ .isTrue();
+ assertThat(TestUtilsKt.equals(actualRequest.getCandidateQueryData(),
+ expectedCandidateQueryDataBundle)).isTrue();
+ assertThat(actualRequest.isSystemProviderRequired()).isEqualTo(expectedSystemProvider);
+ assertThat(actualRequest.isAutoSelectAllowed()).isEqualTo(expectedAutoSelectAllowed);
+ assertThat(actualRequest.getDisplayInfo().getUserId())
+ .isEqualTo(expectedDisplayInfo.getUserId());
+ assertThat(actualRequest.getDisplayInfo().getUserDisplayName())
+ .isEqualTo(expectedDisplayInfo.getUserDisplayName());
+ assertThat(actualRequest.getOrigin()).isEqualTo(expectedOrigin);
+ assertThat(actualRequest.preferImmediatelyAvailableCredentials()).isEqualTo(
+ expectedPreferImmediatelyAvailableCredentials);
+ }
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestTest.kt
index def2dfb..ccc00f3 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestTest.kt
@@ -17,7 +17,9 @@
package androidx.credentials
import android.os.Bundle
+import androidx.credentials.CreateCredentialRequest.Companion.createFrom
import androidx.credentials.CreateCredentialRequest.DisplayInfo
+import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertThrows
import org.junit.Test
@@ -36,24 +38,43 @@
}
}
+ @SdkSuppress(minSdkVersion = 26)
@Test
fun getter() {
val expectedType = "TYPE"
- val expectedCredentialDataBundle = Bundle()
- expectedCredentialDataBundle.putString("Test", "Test")
- val expectedCandidateQueryDataBundle = Bundle()
- expectedCandidateQueryDataBundle.putBoolean("key", true)
+ val expectedAutoSelectAllowed = true
+ val expectedPreferImmediatelyAvailableCredentials = true
+ val inputCredentialDataBundle = Bundle()
+ inputCredentialDataBundle.putString("Test", "Test")
+ val expectedCredentialDataBundle = inputCredentialDataBundle.deepCopy()
+ expectedCredentialDataBundle.putBoolean(
+ CreateCredentialRequest.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
+ expectedAutoSelectAllowed
+ )
+ expectedCredentialDataBundle.putBoolean(
+ CreateCredentialRequest.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+ expectedPreferImmediatelyAvailableCredentials
+ )
+ val inputCandidateQueryDataBundle = Bundle()
+ inputCandidateQueryDataBundle.putBoolean("key", true)
+ val expectedCandidateQueryDataBundle = inputCandidateQueryDataBundle.deepCopy()
+ expectedCandidateQueryDataBundle.putBoolean(
+ CreateCredentialRequest.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
+ expectedAutoSelectAllowed
+ )
val expectedDisplayInfo = DisplayInfo("userId")
- val expectedAutoSelectAllowed = false
val expectedSystemProvider = true
+ val expectedOrigin = "Origin"
val request = CreateCustomCredentialRequest(
expectedType,
- expectedCredentialDataBundle,
- expectedCandidateQueryDataBundle,
+ inputCredentialDataBundle,
+ inputCandidateQueryDataBundle,
expectedSystemProvider,
expectedDisplayInfo,
- expectedAutoSelectAllowed
+ expectedAutoSelectAllowed,
+ expectedOrigin,
+ expectedPreferImmediatelyAvailableCredentials
)
assertThat(request.type).isEqualTo(expectedType)
@@ -65,8 +86,74 @@
expectedCandidateQueryDataBundle
)
).isTrue()
- assertThat(request.isAutoSelectAllowed).isEqualTo(expectedAutoSelectAllowed)
assertThat(request.isSystemProviderRequired).isEqualTo(expectedSystemProvider)
+ assertThat(request.isAutoSelectAllowed).isEqualTo(expectedAutoSelectAllowed)
+ assertThat(request.preferImmediatelyAvailableCredentials).isEqualTo(
+ expectedPreferImmediatelyAvailableCredentials
+ )
assertThat(request.displayInfo).isEqualTo(expectedDisplayInfo)
+ assertThat(request.origin).isEqualTo(expectedOrigin)
+ }
+
+ @SdkSuppress(minSdkVersion = 23)
+ @Test
+ fun frameworkConversion_success() {
+ val expectedType = "TYPE"
+ val expectedCredentialDataBundle = Bundle()
+ expectedCredentialDataBundle.putString("Test", "Test")
+ val expectedCandidateQueryDataBundle = Bundle()
+ expectedCandidateQueryDataBundle.putBoolean("key", true)
+ val expectedDisplayInfo = DisplayInfo("userId")
+ val expectedSystemProvider = true
+ val expectedAutoSelectAllowed = true
+ val expectedPreferImmediatelyAvailableCredentials = true
+ val expectedOrigin = "Origin"
+ val request = CreateCustomCredentialRequest(
+ expectedType,
+ expectedCredentialDataBundle,
+ expectedCandidateQueryDataBundle,
+ expectedSystemProvider,
+ expectedDisplayInfo,
+ expectedAutoSelectAllowed,
+ expectedOrigin,
+ expectedPreferImmediatelyAvailableCredentials,
+ )
+ val finalCredentialData = request.credentialData
+ finalCredentialData.putBundle(
+ DisplayInfo.BUNDLE_KEY_REQUEST_DISPLAY_INFO,
+ expectedDisplayInfo.toBundle()
+ )
+
+ val convertedRequest = createFrom(
+ request.type, request.credentialData, request.candidateQueryData,
+ request.isSystemProviderRequired, request.origin
+ )!!
+
+ assertThat(convertedRequest).isInstanceOf(CreateCustomCredentialRequest::class.java)
+ val actualRequest = convertedRequest as CreateCustomCredentialRequest
+ assertThat(actualRequest.type).isEqualTo(expectedType)
+ assertThat(
+ equals(
+ actualRequest.credentialData,
+ expectedCredentialDataBundle
+ )
+ ).isTrue()
+ assertThat(
+ equals(
+ actualRequest.candidateQueryData,
+ expectedCandidateQueryDataBundle
+ )
+ ).isTrue()
+ assertThat(actualRequest.isSystemProviderRequired).isEqualTo(expectedSystemProvider)
+ assertThat(actualRequest.isAutoSelectAllowed).isEqualTo(expectedAutoSelectAllowed)
+ assertThat(actualRequest.displayInfo.userId)
+ .isEqualTo(expectedDisplayInfo.userId)
+ assertThat(actualRequest.displayInfo.userDisplayName)
+ .isEqualTo(expectedDisplayInfo.userDisplayName)
+ assertThat(actualRequest.origin).isEqualTo(expectedOrigin)
+ assertThat(actualRequest.origin).isEqualTo(expectedOrigin)
+ assertThat(actualRequest.preferImmediatelyAvailableCredentials).isEqualTo(
+ expectedPreferImmediatelyAvailableCredentials
+ )
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
index d002fbd..18a4ee9 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
@@ -16,6 +16,7 @@
package androidx.credentials;
+import static androidx.credentials.CreateCredentialRequest.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS;
import static androidx.credentials.internal.FrameworkImplHelper.getFinalCreateCredentialData;
import static com.google.common.truth.Truth.assertThat;
@@ -56,6 +57,38 @@
}
@Test
+ public void constructor_withDefaults() {
+ String idExpected = "id";
+ String passwordExpected = "password";
+
+ CreatePasswordRequest request = new CreatePasswordRequest(idExpected, passwordExpected);
+
+ assertThat(request.getDisplayInfo().getPreferDefaultProvider()).isNull();
+ assertThat(request.preferImmediatelyAvailableCredentials()).isFalse();
+ assertThat(request.getOrigin()).isNull();
+ assertThat(request.getId()).isEqualTo(idExpected);
+ assertThat(request.getPassword()).isEqualTo(passwordExpected);
+ }
+
+ @Test
+ public void constructor_withoutDefaults() {
+ String idExpected = "id";
+ String passwordExpected = "password";
+ String originExpected = "origin";
+ boolean preferImmediatelyAvailableCredentialsExpected = true;
+
+ CreatePasswordRequest request = new CreatePasswordRequest(idExpected, passwordExpected,
+ originExpected, preferImmediatelyAvailableCredentialsExpected);
+
+ assertThat(request.preferImmediatelyAvailableCredentials())
+ .isEqualTo(preferImmediatelyAvailableCredentialsExpected);
+ assertThat(request.getDisplayInfo().getPreferDefaultProvider()).isNull();
+ assertThat(request.getOrigin()).isEqualTo(originExpected);
+ assertThat(request.getId()).isEqualTo(idExpected);
+ assertThat(request.getPassword()).isEqualTo(passwordExpected);
+ }
+
+ @Test
public void constructor_emptyPassword_throws() {
assertThrows(
IllegalArgumentException.class,
@@ -63,6 +96,28 @@
);
}
+ @SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+ @Test
+ public void constructor_defaultProviderVariant() {
+ String idExpected = "id";
+ String passwordExpected = "pwd";
+ String originExpected = "origin";
+ boolean preferImmediatelyAvailableCredentialsExpected = true;
+ String defaultProviderExpected = "com.test/com.test.TestProviderComponent";
+
+ CreatePasswordRequest request = new CreatePasswordRequest(
+ idExpected, passwordExpected, originExpected, defaultProviderExpected,
+ preferImmediatelyAvailableCredentialsExpected);
+
+ assertThat(request.getDisplayInfo().getPreferDefaultProvider())
+ .isEqualTo(defaultProviderExpected);
+ assertThat(request.preferImmediatelyAvailableCredentials())
+ .isEqualTo(preferImmediatelyAvailableCredentialsExpected);
+ assertThat(request.getOrigin()).isEqualTo(originExpected);
+ assertThat(request.getId()).isEqualTo(idExpected);
+ assertThat(request.getPassword()).isEqualTo(passwordExpected);
+ }
+
@Test
public void getter_id() {
String idExpected = "id";
@@ -83,29 +138,39 @@
public void getter_frameworkProperties() {
String idExpected = "id";
String passwordExpected = "pwd";
- Bundle expectedData = new Bundle();
+ boolean preferImmediatelyAvailableCredentialsExpected = true;
+ Bundle expectedCredentialData = new Bundle();
boolean expectedAutoSelect = false;
- expectedData.putString(CreatePasswordRequest.BUNDLE_KEY_ID, idExpected);
- expectedData.putString(CreatePasswordRequest.BUNDLE_KEY_PASSWORD, passwordExpected);
- expectedData.putBoolean(CreatePasswordRequest.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
+ expectedCredentialData.putString(CreatePasswordRequest.BUNDLE_KEY_ID, idExpected);
+ expectedCredentialData.putString(CreatePasswordRequest.BUNDLE_KEY_PASSWORD,
+ passwordExpected);
+ expectedCredentialData.putBoolean(CreatePasswordRequest.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
+ expectedAutoSelect);
+ expectedCredentialData.putBoolean(
+ BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+ preferImmediatelyAvailableCredentialsExpected);
+ Bundle expectedCandidateData = new Bundle();
+ expectedCandidateData.putBoolean(CreatePasswordRequest.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
expectedAutoSelect);
- CreatePasswordRequest request = new CreatePasswordRequest(idExpected, passwordExpected);
+ CreatePasswordRequest request = new CreatePasswordRequest(idExpected, passwordExpected,
+ /*origin=*/ null, preferImmediatelyAvailableCredentialsExpected);
assertThat(request.getType()).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL);
CreateCredentialRequest.DisplayInfo displayInfo =
request.getDisplayInfo();
assertThat(displayInfo.getUserDisplayName()).isNull();
assertThat(displayInfo.getUserId()).isEqualTo(idExpected);
- assertThat(TestUtilsKt.equals(request.getCandidateQueryData(), Bundle.EMPTY)).isTrue();
+ assertThat(TestUtilsKt.equals(request.getCandidateQueryData(), expectedCandidateData))
+ .isTrue();
assertThat(request.isSystemProviderRequired()).isFalse();
Bundle credentialData =
getFinalCreateCredentialData(
request, mContext);
assertThat(credentialData.keySet())
- .hasSize(expectedData.size() + /* added request info */ 1);
- for (String key : expectedData.keySet()) {
- assertThat(credentialData.get(key)).isEqualTo(credentialData.get(key));
+ .hasSize(expectedCredentialData.size() + /* added request info */ 1);
+ for (String key : expectedCredentialData.keySet()) {
+ assertThat(credentialData.get(key)).isEqualTo(expectedCredentialData.get(key));
}
Bundle displayInfoBundle =
credentialData.getBundle(
@@ -122,7 +187,13 @@
@Test
public void frameworkConversion_success() {
String idExpected = "id";
- CreatePasswordRequest request = new CreatePasswordRequest(idExpected, "password");
+ String passwordExpected = "pwd";
+ boolean preferImmediatelyAvailableCredentialsExpected = true;
+ String originExpected = "origin";
+ String defaultProviderExpected = "com.test/com.test.TestProviderComponent";
+ CreatePasswordRequest request = new CreatePasswordRequest(
+ idExpected, passwordExpected, originExpected, defaultProviderExpected,
+ preferImmediatelyAvailableCredentialsExpected);
CreateCredentialRequest convertedRequest = CreateCredentialRequest.createFrom(
request.getType(), getFinalCreateCredentialData(
@@ -134,13 +205,17 @@
assertThat(convertedRequest).isInstanceOf(CreatePasswordRequest.class);
CreatePasswordRequest convertedCreatePasswordRequest =
(CreatePasswordRequest) convertedRequest;
- assertThat(convertedCreatePasswordRequest.getPassword()).isEqualTo(request.getPassword());
- assertThat(convertedCreatePasswordRequest.getId()).isEqualTo(request.getId());
+ assertThat(convertedCreatePasswordRequest.getPassword()).isEqualTo(passwordExpected);
+ assertThat(convertedCreatePasswordRequest.getId()).isEqualTo(idExpected);
+ assertThat(convertedCreatePasswordRequest.preferImmediatelyAvailableCredentials())
+ .isEqualTo(preferImmediatelyAvailableCredentialsExpected);
+ assertThat(convertedCreatePasswordRequest.getOrigin()).isEqualTo(originExpected);
CreateCredentialRequest.DisplayInfo displayInfo =
convertedCreatePasswordRequest.getDisplayInfo();
assertThat(displayInfo.getUserDisplayName()).isNull();
assertThat(displayInfo.getUserId()).isEqualTo(idExpected);
assertThat(displayInfo.getCredentialTypeIcon().getResId())
.isEqualTo(R.drawable.ic_password);
+ assertThat(displayInfo.getPreferDefaultProvider()).isEqualTo(defaultProviderExpected);
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
index 6aeb65d..a9f76dc 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
@@ -18,7 +18,9 @@
import android.graphics.drawable.Icon
import android.os.Bundle
+import android.os.Parcelable
import androidx.credentials.CreateCredentialRequest.Companion.createFrom
+import androidx.credentials.CreateCredentialRequest.DisplayInfo
import androidx.credentials.internal.FrameworkImplHelper.Companion.getFinalCreateCredentialData
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
@@ -43,6 +45,64 @@
}
@Test
+ fun constructor_withDefaults() {
+ val idExpected = "id"
+ val passwordExpected = "password"
+
+ val request = CreatePasswordRequest(idExpected, passwordExpected)
+
+ assertThat(request.displayInfo.preferDefaultProvider).isNull()
+ assertThat(request.preferImmediatelyAvailableCredentials).isFalse()
+ assertThat(request.origin).isNull()
+ assertThat(request.id).isEqualTo(idExpected)
+ assertThat(request.password).isEqualTo(passwordExpected)
+ }
+
+ @Test
+ fun constructor_withoutDefaults() {
+ val idExpected = "id"
+ val passwordExpected = "password"
+ val originExpected = "origin"
+ val preferImmediatelyAvailableCredentialsExpected = true
+
+ val request = CreatePasswordRequest(
+ idExpected, passwordExpected,
+ originExpected, preferImmediatelyAvailableCredentialsExpected
+ )
+
+ assertThat(request.preferImmediatelyAvailableCredentials)
+ .isEqualTo(preferImmediatelyAvailableCredentialsExpected)
+ assertThat(request.displayInfo.preferDefaultProvider).isNull()
+ assertThat(request.origin).isEqualTo(originExpected)
+ assertThat(request.id).isEqualTo(idExpected)
+ assertThat(request.password).isEqualTo(passwordExpected)
+ }
+
+ @Test
+ fun constructor_defaultProviderVariant() {
+ val idExpected = "id"
+ val passwordExpected = "pwd"
+ val originExpected = "origin"
+ val defaultProviderExpected = "com.test/com.test.TestProviderComponent"
+ val preferImmediatelyAvailableCredentialsExpected = true
+
+ val request = CreatePasswordRequest(
+ id = idExpected,
+ password = passwordExpected,
+ origin = originExpected,
+ preferDefaultProvider = defaultProviderExpected,
+ preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentialsExpected,
+ )
+
+ assertThat(request.displayInfo.preferDefaultProvider).isEqualTo(defaultProviderExpected)
+ assertThat(request.origin).isEqualTo(originExpected)
+ assertThat(request.password).isEqualTo(passwordExpected)
+ assertThat(request.id).isEqualTo(idExpected)
+ assertThat(request.preferImmediatelyAvailableCredentials)
+ .isEqualTo(preferImmediatelyAvailableCredentialsExpected)
+ }
+
+ @Test
fun getter_id() {
val idExpected = "id"
val request = CreatePasswordRequest(idExpected, "password")
@@ -62,6 +122,7 @@
fun getter_frameworkProperties() {
val idExpected = "id"
val passwordExpected = "pwd"
+ val preferImmediatelyAvailableCredentialsExpected = true
val expectedCredentialData = Bundle()
val expectedAutoSelect = false
expectedCredentialData.putString(CreatePasswordRequest.BUNDLE_KEY_ID, idExpected)
@@ -69,32 +130,53 @@
CreatePasswordRequest.BUNDLE_KEY_PASSWORD,
passwordExpected
)
- expectedCredentialData.putBoolean(CreateCredentialRequest.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
- expectedAutoSelect)
+ expectedCredentialData.putBoolean(
+ CreateCredentialRequest.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
+ expectedAutoSelect
+ )
+ expectedCredentialData.putBoolean(
+ CreateCredentialRequest.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+ preferImmediatelyAvailableCredentialsExpected
+ )
+ val expectedCandidateData = Bundle()
+ expectedCandidateData.putBoolean(
+ CreateCredentialRequest.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
+ expectedAutoSelect
+ )
- val request = CreatePasswordRequest(idExpected, passwordExpected)
+ val request = CreatePasswordRequest(
+ idExpected, passwordExpected, /*origin=*/null,
+ preferImmediatelyAvailableCredentialsExpected
+ )
assertThat(request.type).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL)
- assertThat(equals(request.candidateQueryData, Bundle.EMPTY)).isTrue()
+ val displayInfo = request.displayInfo
+ assertThat(displayInfo.userDisplayName).isNull()
+ assertThat(displayInfo.userId).isEqualTo(idExpected)
+ assertThat(equals(request.candidateQueryData, expectedCandidateData))
+ .isTrue()
assertThat(request.isSystemProviderRequired).isFalse()
- assertThat(request.displayInfo.userDisplayName).isNull()
- assertThat(request.displayInfo.userId).isEqualTo(idExpected)
val credentialData = getFinalCreateCredentialData(
request, mContext
)
assertThat(credentialData.keySet())
- .hasSize(expectedCredentialData.size() + /* added request info */ 1)
+ .hasSize(expectedCredentialData.size() + /* added request info */1)
for (key in expectedCredentialData.keySet()) {
- assertThat(expectedCredentialData.get(key)).isEqualTo(credentialData.get(key))
+ assertThat(credentialData[key]).isEqualTo(expectedCredentialData[key])
}
- val displayInfoBundle =
- credentialData.getBundle(
- CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_REQUEST_DISPLAY_INFO)!!
- assertThat(displayInfoBundle.keySet()).hasSize(2)
- assertThat(displayInfoBundle.getString(
- CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_USER_ID)).isEqualTo(idExpected)
- assertThat((displayInfoBundle.getParcelable(
- CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_CREDENTIAL_TYPE_ICON) as Icon?)!!.resId
+ val displayInfoBundle = credentialData.getBundle(
+ DisplayInfo.BUNDLE_KEY_REQUEST_DISPLAY_INFO
+ )
+ assertThat(displayInfoBundle!!.keySet()).hasSize(2)
+ assertThat(
+ displayInfoBundle.getString(
+ DisplayInfo.BUNDLE_KEY_USER_ID
+ )
+ ).isEqualTo(idExpected)
+ assertThat(
+ (displayInfoBundle.getParcelable<Parcelable>(
+ DisplayInfo.BUNDLE_KEY_CREDENTIAL_TYPE_ICON
+ ) as Icon?)!!.resId
).isEqualTo(R.drawable.ic_password)
}
@@ -102,27 +184,37 @@
@Test
fun frameworkConversion_success() {
val idExpected = "id"
- val request = CreatePasswordRequest(idExpected, "password")
- val origin = "origin"
+ val passwordExpected = "pwd"
+ val preferImmediatelyAvailableCredentialsExpected = true
+ val originExpected = "origin"
+ val defaultProviderExpected = "com.test/com.test.TestProviderComponent"
+ val request = CreatePasswordRequest(
+ idExpected, passwordExpected, originExpected, defaultProviderExpected,
+ preferImmediatelyAvailableCredentialsExpected
+ )
val convertedRequest = createFrom(
request.type, getFinalCreateCredentialData(
request, mContext
),
request.candidateQueryData, request.isSystemProviderRequired,
- origin
+ request.origin
)
assertThat(convertedRequest).isInstanceOf(
CreatePasswordRequest::class.java
)
val convertedCreatePasswordRequest = convertedRequest as CreatePasswordRequest
- assertThat(convertedCreatePasswordRequest.password).isEqualTo(request.password)
- assertThat(convertedCreatePasswordRequest.id).isEqualTo(request.id)
- assertThat(convertedCreatePasswordRequest.displayInfo.userDisplayName).isNull()
- assertThat(convertedCreatePasswordRequest.displayInfo.userId).isEqualTo(idExpected)
- assertThat(convertedCreatePasswordRequest.displayInfo.credentialTypeIcon?.resId)
+ assertThat(convertedCreatePasswordRequest.password).isEqualTo(passwordExpected)
+ assertThat(convertedCreatePasswordRequest.id).isEqualTo(idExpected)
+ assertThat(convertedCreatePasswordRequest.preferImmediatelyAvailableCredentials)
+ .isEqualTo(preferImmediatelyAvailableCredentialsExpected)
+ assertThat(convertedCreatePasswordRequest.origin).isEqualTo(originExpected)
+ val displayInfo = convertedCreatePasswordRequest.displayInfo
+ assertThat(displayInfo.userDisplayName).isNull()
+ assertThat(displayInfo.userId).isEqualTo(idExpected)
+ assertThat(displayInfo.credentialTypeIcon!!.resId)
.isEqualTo(R.drawable.ic_password)
- assertThat(convertedCreatePasswordRequest.origin).isEqualTo(origin)
+ assertThat(displayInfo.preferDefaultProvider).isEqualTo(defaultProviderExpected)
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
index d3af566..c4dba09 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
@@ -16,8 +16,8 @@
package androidx.credentials;
+import static androidx.credentials.CreateCredentialRequest.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS;
import static androidx.credentials.CreatePublicKeyCredentialRequest.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED;
-import static androidx.credentials.CreatePublicKeyCredentialRequest.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS;
import static androidx.credentials.CreatePublicKeyCredentialRequest.BUNDLE_KEY_REQUEST_JSON;
import static androidx.credentials.internal.FrameworkImplHelper.getFinalCreateCredentialData;
@@ -82,6 +82,27 @@
}
@Test
+ public void constructor_defaultProviderVariant() {
+ byte[] clientDataHashExpected = "hash".getBytes();
+ String originExpected = "origin";
+ Boolean preferImmediatelyAvailableCredentialsExpected = true;
+ String defaultProviderExpected = "com.test/com.test.TestProviderComponent";
+
+ CreatePublicKeyCredentialRequest request = new CreatePublicKeyCredentialRequest(
+ TEST_REQUEST_JSON, clientDataHashExpected,
+ preferImmediatelyAvailableCredentialsExpected, originExpected,
+ defaultProviderExpected);
+
+ assertThat(request.getDisplayInfo().getPreferDefaultProvider())
+ .isEqualTo(defaultProviderExpected);
+ assertThat(request.getClientDataHash()).isEqualTo(clientDataHashExpected);
+ assertThat(request.getOrigin()).isEqualTo(originExpected);
+ assertThat(request.getRequestJson()).isEqualTo(TEST_REQUEST_JSON);
+ assertThat(request.preferImmediatelyAvailableCredentials())
+ .isEqualTo(preferImmediatelyAvailableCredentialsExpected);
+ }
+
+ @Test
public void constructor_setsPreferImmediatelyAvailableCredentialsToFalseByDefault() {
CreatePublicKeyCredentialRequest createPublicKeyCredentialRequest =
new CreatePublicKeyCredentialRequest(TEST_REQUEST_JSON);
@@ -93,7 +114,7 @@
@Test
public void constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
boolean preferImmediatelyAvailableCredentialsExpected = true;
- String clientDataHash = "hash";
+ byte[] clientDataHash = "hash".getBytes();
CreatePublicKeyCredentialRequest createPublicKeyCredentialRequest =
new CreatePublicKeyCredentialRequest(TEST_REQUEST_JSON,
clientDataHash,
@@ -119,38 +140,40 @@
@Test
public void getter_frameworkProperties_success() {
String requestJsonExpected = TEST_REQUEST_JSON;
- String clientDataHash = "hash";
- boolean preferImmediatelyAvailableCredentialsExpected = false;
- Bundle expectedData = new Bundle();
- expectedData.putString(
+ byte[] clientDataHash = "hash".getBytes();
+ boolean preferImmediatelyAvailableCredentialsExpected = true;
+ boolean autoSelectExpected = false;
+ Bundle expectedCandidateQueryData = new Bundle();
+ expectedCandidateQueryData.putString(
PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
CreatePublicKeyCredentialRequest
.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST);
- expectedData.putString(
+ expectedCandidateQueryData.putString(
BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
- expectedData.putString(CreatePublicKeyCredentialRequest.BUNDLE_KEY_CLIENT_DATA_HASH,
+ expectedCandidateQueryData.putByteArray(
+ CreatePublicKeyCredentialRequest.BUNDLE_KEY_CLIENT_DATA_HASH,
clientDataHash);
- expectedData.putBoolean(
+ expectedCandidateQueryData.putBoolean(
+ BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
+ autoSelectExpected);
+ Bundle expectedCredentialData = expectedCandidateQueryData.deepCopy();
+ expectedCredentialData.putBoolean(
BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
preferImmediatelyAvailableCredentialsExpected);
- expectedData.putBoolean(
- BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
- preferImmediatelyAvailableCredentialsExpected);
- Bundle expectedQuery = TestUtilsKt.deepCopyBundle(expectedData);
- expectedQuery.remove(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED);
CreatePublicKeyCredentialRequest request = new CreatePublicKeyCredentialRequest(
requestJsonExpected, clientDataHash, preferImmediatelyAvailableCredentialsExpected);
assertThat(request.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
- assertThat(TestUtilsKt.equals(request.getCandidateQueryData(), expectedQuery)).isTrue();
+ assertThat(TestUtilsKt.equals(request.getCandidateQueryData(), expectedCandidateQueryData))
+ .isTrue();
assertThat(request.isSystemProviderRequired()).isFalse();
Bundle credentialData = getFinalCreateCredentialData(
request, mContext);
assertThat(credentialData.keySet())
- .hasSize(expectedData.size() + /* added request info */ 1);
- for (String key : expectedData.keySet()) {
- assertThat(credentialData.get(key)).isEqualTo(credentialData.get(key));
+ .hasSize(expectedCredentialData.size() + /* added request info */ 1);
+ for (String key : expectedCredentialData.keySet()) {
+ assertThat(credentialData.get(key)).isEqualTo(expectedCredentialData.get(key));
}
Bundle displayInfoBundle =
credentialData.getBundle(
@@ -169,9 +192,12 @@
@SdkSuppress(minSdkVersion = 28)
@Test
public void frameworkConversion_success() {
- String clientDataHash = "hash";
- CreatePublicKeyCredentialRequest request =
- new CreatePublicKeyCredentialRequest(TEST_REQUEST_JSON, clientDataHash, true);
+ byte[] clientDataHashExpected = "hash".getBytes();
+ String originExpected = "origin";
+ Boolean preferImmediatelyAvailableCredentialsExpected = true;
+ CreatePublicKeyCredentialRequest request = new CreatePublicKeyCredentialRequest(
+ TEST_REQUEST_JSON, clientDataHashExpected,
+ preferImmediatelyAvailableCredentialsExpected, originExpected);
CreateCredentialRequest convertedRequest = CreateCredentialRequest.createFrom(
request.getType(), getFinalCreateCredentialData(
@@ -184,8 +210,10 @@
CreatePublicKeyCredentialRequest convertedSubclassRequest =
(CreatePublicKeyCredentialRequest) convertedRequest;
assertThat(convertedSubclassRequest.getRequestJson()).isEqualTo(request.getRequestJson());
+ assertThat(convertedSubclassRequest.getOrigin()).isEqualTo(originExpected);
+ assertThat(convertedSubclassRequest.getClientDataHash()).isEqualTo(clientDataHashExpected);
assertThat(convertedSubclassRequest.preferImmediatelyAvailableCredentials())
- .isEqualTo(request.preferImmediatelyAvailableCredentials());
+ .isEqualTo(preferImmediatelyAvailableCredentialsExpected);
CreateCredentialRequest.DisplayInfo displayInfo =
convertedRequest.getDisplayInfo();
assertThat(displayInfo.getUserDisplayName()).isEqualTo(TEST_USER_DISPLAYNAME);
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
index 538da6a..1f5a3df 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
@@ -19,8 +19,8 @@
import android.graphics.drawable.Icon
import android.os.Bundle
import android.os.Parcelable
+import androidx.credentials.CreateCredentialRequest.Companion.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS
import androidx.credentials.CreateCredentialRequest.Companion.createFrom
-import androidx.credentials.CreatePublicKeyCredentialRequest.Companion.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS
import androidx.credentials.CreatePublicKeyCredentialRequest.Companion.BUNDLE_KEY_REQUEST_JSON
import androidx.credentials.internal.FrameworkImplHelper.Companion.getFinalCreateCredentialData
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -74,13 +74,15 @@
fun constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
val preferImmediatelyAvailableCredentialsExpected = true
val origin = "origin"
- val clientDataHash = "hash"
+ val clientDataHash = "hash".toByteArray()
+
val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest(
TEST_REQUEST_JSON,
clientDataHash,
preferImmediatelyAvailableCredentialsExpected,
origin
)
+
val preferImmediatelyAvailableCredentialsActual =
createPublicKeyCredentialRequest.preferImmediatelyAvailableCredentials
assertThat(preferImmediatelyAvailableCredentialsActual)
@@ -89,6 +91,28 @@
}
@Test
+ fun constructor_defaultProviderVariant() {
+ val clientDataHashExpected = "hash".toByteArray()
+ val originExpected = "origin"
+ val preferImmediatelyAvailableCredentialsExpected = true
+ val defaultProviderExpected = "com.test/com.test.TestProviderComponent"
+
+ val request = CreatePublicKeyCredentialRequest(
+ TEST_REQUEST_JSON, clientDataHashExpected,
+ preferImmediatelyAvailableCredentialsExpected, originExpected,
+ defaultProviderExpected
+ )
+
+ assertThat(request.displayInfo.preferDefaultProvider)
+ .isEqualTo(defaultProviderExpected)
+ assertThat(request.clientDataHash).isEqualTo(clientDataHashExpected)
+ assertThat(request.origin).isEqualTo(originExpected)
+ assertThat(request.requestJson).isEqualTo(TEST_REQUEST_JSON)
+ assertThat(request.preferImmediatelyAvailableCredentials)
+ .isEqualTo(preferImmediatelyAvailableCredentialsExpected)
+ }
+
+ @Test
fun getter_requestJson_success() {
val testJsonExpected = "{\"user\":{\"name\":{\"lol\":\"Value\"}}}"
val createPublicKeyCredentialReq = CreatePublicKeyCredentialRequest(testJsonExpected)
@@ -101,58 +125,52 @@
@Test
fun getter_frameworkProperties_success() {
val requestJsonExpected = TEST_REQUEST_JSON
- val preferImmediatelyAvailableCredentialsExpected = false
- val expectedAutoSelect = true
- val origin = "origin"
- val clientDataHash = "hash"
- val expectedData = Bundle()
- expectedData.putString(
+ val clientDataHash = "hash".toByteArray()
+ val preferImmediatelyAvailableCredentialsExpected = true
+ val autoSelectExpected = false
+ val expectedCandidateQueryData = Bundle()
+ expectedCandidateQueryData.putString(
PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
CreatePublicKeyCredentialRequest
.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST
)
- expectedData.putString(
+ expectedCandidateQueryData.putString(
BUNDLE_KEY_REQUEST_JSON, requestJsonExpected
)
- expectedData.putString(CreatePublicKeyCredentialRequest.BUNDLE_KEY_CLIENT_DATA_HASH,
- clientDataHash)
- expectedData.putBoolean(
+ expectedCandidateQueryData.putByteArray(
+ CreatePublicKeyCredentialRequest.BUNDLE_KEY_CLIENT_DATA_HASH,
+ clientDataHash
+ )
+ expectedCandidateQueryData.putBoolean(
+ CreateCredentialRequest.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
+ autoSelectExpected
+ )
+ val expectedCredentialData = expectedCandidateQueryData.deepCopy()
+ expectedCredentialData.putBoolean(
BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
preferImmediatelyAvailableCredentialsExpected
)
- expectedData.putBoolean(
- CreateCredentialRequest.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
- expectedAutoSelect
- )
-
- val expectedCandidateQueryBundle = expectedData.deepCopy()
- expectedCandidateQueryBundle.remove(
- CreateCredentialRequest.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED
- )
val request = CreatePublicKeyCredentialRequest(
- requestJsonExpected,
- clientDataHash,
- preferImmediatelyAvailableCredentialsExpected,
- origin
+ requestJsonExpected, clientDataHash, preferImmediatelyAvailableCredentialsExpected
)
assertThat(request.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
- assertThat(equals(request.candidateQueryData, expectedCandidateQueryBundle)).isTrue()
+ assertThat(equals(request.candidateQueryData, expectedCandidateQueryData))
+ .isTrue()
assertThat(request.isSystemProviderRequired).isFalse()
- assertThat(request.origin).isEqualTo(origin)
val credentialData = getFinalCreateCredentialData(
request, mContext
)
assertThat(credentialData.keySet())
- .hasSize(expectedData.size() + /* added request info */1)
- for (key in expectedData.keySet()) {
- assertThat(credentialData[key]).isEqualTo(credentialData[key])
+ .hasSize(expectedCredentialData.size() + /* added request info */1)
+ for (key in expectedCredentialData.keySet()) {
+ assertThat(credentialData[key]).isEqualTo(expectedCredentialData[key])
}
val displayInfoBundle = credentialData.getBundle(
CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_REQUEST_DISPLAY_INFO
- )!!
- assertThat(displayInfoBundle.keySet()).hasSize(3)
+ )
+ assertThat(displayInfoBundle!!.keySet()).hasSize(3)
assertThat(
displayInfoBundle.getString(
CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_USER_ID
@@ -173,10 +191,13 @@
@SdkSuppress(minSdkVersion = 28)
@Test
fun frameworkConversion_success() {
- val origin = "origin"
- val clientDataHash = "hash"
- val request = CreatePublicKeyCredentialRequest(TEST_REQUEST_JSON, clientDataHash,
- true, origin)
+ val clientDataHashExpected = "hash".toByteArray()
+ val originExpected = "origin"
+ val preferImmediatelyAvailableCredentialsExpected = true
+ val request = CreatePublicKeyCredentialRequest(
+ TEST_REQUEST_JSON, clientDataHashExpected,
+ preferImmediatelyAvailableCredentialsExpected, originExpected
+ )
val convertedRequest = createFrom(
request.type, getFinalCreateCredentialData(
@@ -189,15 +210,16 @@
assertThat(convertedRequest).isInstanceOf(
CreatePublicKeyCredentialRequest::class.java
)
- assertThat(convertedRequest?.origin).isEqualTo(origin)
val convertedSubclassRequest = convertedRequest as CreatePublicKeyCredentialRequest
assertThat(convertedSubclassRequest.requestJson).isEqualTo(request.requestJson)
+ assertThat(convertedSubclassRequest.origin).isEqualTo(originExpected)
+ assertThat(convertedSubclassRequest.clientDataHash).isEqualTo(clientDataHashExpected)
assertThat(convertedSubclassRequest.preferImmediatelyAvailableCredentials)
- .isEqualTo(request.preferImmediatelyAvailableCredentials)
- val displayInfo = convertedRequest.displayInfo
+ .isEqualTo(preferImmediatelyAvailableCredentialsExpected)
+ val displayInfo = convertedSubclassRequest.displayInfo
assertThat(displayInfo.userDisplayName).isEqualTo(TEST_USER_DISPLAYNAME)
assertThat(displayInfo.userId).isEqualTo(TEST_USERNAME)
- assertThat(displayInfo.credentialTypeIcon?.resId)
+ assertThat(displayInfo.credentialTypeIcon!!.resId)
.isEqualTo(R.drawable.ic_passkey)
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerJavaTest.java
index 85246a1..1a8f8a0 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerJavaTest.java
@@ -25,14 +25,18 @@
import android.os.Looper;
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
import androidx.credentials.exceptions.ClearCredentialException;
import androidx.credentials.exceptions.ClearCredentialProviderConfigurationException;
import androidx.credentials.exceptions.CreateCredentialException;
+import androidx.credentials.exceptions.CreateCredentialNoCreateOptionException;
import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException;
import androidx.credentials.exceptions.GetCredentialException;
import androidx.credentials.exceptions.GetCredentialProviderConfigurationException;
+import androidx.credentials.exceptions.NoCredentialException;
import androidx.test.core.app.ActivityScenario;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -46,6 +50,8 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
+@RequiresApi(16)
+@SdkSuppress(minSdkVersion = 16)
public class CredentialManagerJavaTest {
private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
@@ -68,8 +74,8 @@
ActivityScenario.launch(TestActivity.class);
activityScenario.onActivity(activity -> {
mCredentialManager.createCredentialAsync(
- new CreatePasswordRequest("test-user-id", "test-password"),
activity,
+ new CreatePasswordRequest("test-user-id", "test-password"),
null,
Runnable::run,
new CredentialManagerCallback<CreateCredentialResponse,
@@ -81,20 +87,26 @@
}
@Override
- public void onResult(@NonNull CreateCredentialResponse result) {}
+ public void onResult(@NonNull CreateCredentialResponse result) {
+ }
});
});
latch.await(100L, TimeUnit.MILLISECONDS);
- assertThat(loadedResult.get().getClass()).isEqualTo(
- CreateCredentialProviderConfigurationException.class);
+ if (!isPostFrameworkApiLevel()) {
+ assertThat(loadedResult.get().getClass()).isEqualTo(
+ CreateCredentialProviderConfigurationException.class);
+ } else {
+ assertThat(loadedResult.get().getClass()).isEqualTo(
+ CreateCredentialNoCreateOptionException.class);
+ }
// TODO("Add manifest tests and possibly further separate these tests by API Level
// - maybe a rule perhaps?")
}
-
@Test
- public void testGetCredentialAsyc_successCallbackThrows() throws InterruptedException {
+ public void testGetCredentialAsyc_requestBasedApi_successCallbackThrows()
+ throws InterruptedException {
if (Looper.myLooper() == null) {
Looper.prepare();
}
@@ -102,32 +114,96 @@
AtomicReference<GetCredentialException> loadedResult = new AtomicReference<>();
mCredentialManager.getCredentialAsync(
+ new Activity(),
new GetCredentialRequest.Builder()
.addCredentialOption(new GetPasswordOption())
.build(),
- new Activity(),
null,
Runnable::run,
new CredentialManagerCallback<GetCredentialResponse,
- GetCredentialException>() {
- @Override
- public void onError(@NonNull GetCredentialException e) {
- loadedResult.set(e);
- latch.countDown();
- }
+ GetCredentialException>() {
+ @Override
+ public void onError(@NonNull GetCredentialException e) {
+ loadedResult.set(e);
+ latch.countDown();
+ }
- @Override
- public void onResult(@NonNull GetCredentialResponse result) {}
- });
+ @Override
+ public void onResult(@NonNull GetCredentialResponse result) {
+ }
+ });
latch.await(100L, TimeUnit.MILLISECONDS);
- assertThat(loadedResult.get().getClass()).isEqualTo(
- GetCredentialProviderConfigurationException.class);
+ if (!isPostFrameworkApiLevel()) {
+ assertThat(loadedResult.get().getClass()).isEqualTo(
+ GetCredentialProviderConfigurationException.class);
+ } else {
+ assertThat(loadedResult.get().getClass()).isEqualTo(
+ NoCredentialException.class);
+ }
// TODO("Add manifest tests and possibly further separate these tests - maybe a rule
// perhaps?")
}
@Test
+ @SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+ public void testPrepareGetCredentialAsyc_throwsUnimplementedError() throws Exception {
+ CountDownLatch latch1 = new CountDownLatch(1);
+ AtomicReference<PrepareGetCredentialResponse> prepareResult = new AtomicReference<>();
+
+ mCredentialManager.prepareGetCredentialAsync(
+ new GetCredentialRequest.Builder()
+ .addCredentialOption(new GetPasswordOption())
+ .build(),
+ null,
+ Runnable::run,
+ new CredentialManagerCallback<PrepareGetCredentialResponse,
+ GetCredentialException>() {
+ @Override
+ public void onError(@NonNull GetCredentialException e) {}
+
+ @Override
+ public void onResult(@NonNull PrepareGetCredentialResponse result) {
+ prepareResult.set(result);
+ latch1.countDown();
+ }
+ });
+ latch1.await(100L, TimeUnit.MILLISECONDS);
+ assertThat(prepareResult.get()).isNotNull();
+
+
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ CountDownLatch latch2 = new CountDownLatch(1);
+ AtomicReference<GetCredentialException> getResult = new AtomicReference<>();
+
+ ActivityScenario<TestActivity> activityScenario =
+ ActivityScenario.launch(TestActivity.class);
+ activityScenario.onActivity(activity -> {
+ mCredentialManager.getCredentialAsync(
+ activity,
+ prepareResult.get().getPendingGetCredentialHandle(),
+ null,
+ Runnable::run,
+ new CredentialManagerCallback<GetCredentialResponse,
+ GetCredentialException>() {
+ @Override
+ public void onError(@NonNull GetCredentialException e) {
+ getResult.set(e);
+ latch2.countDown();
+ }
+
+ @Override
+ public void onResult(@NonNull GetCredentialResponse result) {}
+ });
+ });
+
+ latch2.await(100L, TimeUnit.MILLISECONDS);
+ assertThat(getResult.get().getClass()).isEqualTo(NoCredentialException.class);
+ }
+
+ @Test
public void testClearCredentialSessionAsync_throws() throws InterruptedException {
if (isPostFrameworkApiLevel()) {
return; // TODO(Support!)
@@ -148,7 +224,8 @@
}
@Override
- public void onResult(@NonNull Void result) {}
+ public void onResult(@NonNull Void result) {
+ }
});
latch.await(100L, TimeUnit.MILLISECONDS);
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerTest.kt
index 162d6df..e841b77 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerTest.kt
@@ -18,17 +18,22 @@
import android.app.Activity
import android.os.Looper
+import androidx.annotation.RequiresApi
import androidx.credentials.exceptions.ClearCredentialException
import androidx.credentials.exceptions.ClearCredentialProviderConfigurationException
import androidx.credentials.exceptions.CreateCredentialException
+import androidx.credentials.exceptions.CreateCredentialNoCreateOptionException
import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException
-import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
+import androidx.credentials.exceptions.NoCredentialException
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.testutils.assertThrows
+import androidx.testutils.withActivity
+import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executor
@@ -41,6 +46,8 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
+@RequiresApi(16)
+@SdkSuppress(minSdkVersion = 16)
class CredentialManagerTest {
private val context = InstrumentationRegistry.getInstrumentation().context
@@ -59,8 +66,8 @@
if (!isPostFrameworkApiLevel()) {
assertThrows<CreateCredentialProviderConfigurationException> {
credentialManager.createCredential(
- CreatePasswordRequest("test-user-id", "test-password"),
- Activity()
+ Activity(),
+ CreatePasswordRequest("test-user-id", "test-password")
)
}
}
@@ -69,7 +76,7 @@
}
@Test
- fun getCredential_throws() = runBlocking<Unit> {
+ fun getCredential_requestBasedApi_throws() = runBlocking<Unit> {
if (Looper.myLooper() == null) {
Looper.prepare()
}
@@ -79,7 +86,11 @@
if (!isPostFrameworkApiLevel()) {
assertThrows<GetCredentialProviderConfigurationException> {
- credentialManager.getCredential(request, Activity())
+ credentialManager.getCredential(Activity(), request)
+ }
+ } else {
+ assertThrows<NoCredentialException> {
+ credentialManager.getCredential(Activity(), request)
}
}
// TODO("Add manifest tests and possibly further separate these tests by API Level
@@ -87,6 +98,30 @@
}
@Test
+ @SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+ fun testPrepareGetCredential_throwsUnimplementedError() = runBlocking<Unit> {
+ val prepareGetCredentialResponse = credentialManager.prepareGetCredential(
+ GetCredentialRequest(listOf(GetPasswordOption())))
+
+ if (Looper.myLooper() == null) {
+ Looper.prepare()
+ }
+
+ withUse(ActivityScenario.launch(TestActivity::class.java)) {
+ withActivity {
+ runBlocking {
+ assertThrows<NoCredentialException> {
+ credentialManager.getCredential(
+ this@withActivity,
+ prepareGetCredentialResponse.pendingGetCredentialHandle
+ )
+ }
+ }
+ }
+ }
+ }
+
+ @Test
fun testClearCredentialSession_throws() = runBlocking<Unit> {
if (Looper.myLooper() == null) {
Looper.prepare()
@@ -114,8 +149,8 @@
activityScenario.onActivity { activity ->
credentialManager.createCredentialAsync(
- CreatePasswordRequest("test-user-id", "test-password"),
activity,
+ CreatePasswordRequest("test-user-id", "test-password"),
null, Executor { obj: Runnable -> obj.run() },
object : CredentialManagerCallback<CreateCredentialResponse,
CreateCredentialException> {
@@ -132,47 +167,16 @@
assertThat(loadedResult.get().javaClass).isEqualTo(
CreateCredentialProviderConfigurationException::class.java
)
+ } else {
+ assertThat(loadedResult.get().javaClass).isEqualTo(
+ CreateCredentialNoCreateOptionException::class.java
+ )
}
// TODO("Add manifest tests and possibly further separate these tests by API Level
// - maybe a rule perhaps?")
}
@Test
- fun testGetCredentialAsyc_successCallbackThrows() {
- if (Looper.myLooper() == null) {
- Looper.prepare()
- }
- val latch = CountDownLatch(1)
- val loadedResult: AtomicReference<GetCredentialException> = AtomicReference()
-
- credentialManager.getCredentialAsync(
- request = GetCredentialRequest.Builder()
- .addCredentialOption(GetPasswordOption())
- .build(),
- activity = Activity(),
- cancellationSignal = null,
- executor = Runnable::run,
- callback = object : CredentialManagerCallback<GetCredentialResponse,
- GetCredentialException> {
- override fun onResult(result: GetCredentialResponse) {}
- override fun onError(e: GetCredentialException) {
- loadedResult.set(e)
- latch.countDown()
- }
- }
- )
-
- latch.await(100L, TimeUnit.MILLISECONDS)
- if (!isPostFrameworkApiLevel()) {
- assertThat(loadedResult.get().javaClass).isEqualTo(
- GetCredentialProviderConfigurationException::class.java
- )
- }
- // TODO("Add manifest tests and possibly further separate these tests - maybe a rule
- // perhaps?")
- }
-
- @Test
fun testClearCredentialSessionAsync_throws() {
if (Looper.myLooper() == null) {
Looper.prepare()
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestJavaTest.java
index e8bea47..887aabb 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestJavaTest.java
@@ -20,6 +20,8 @@
import static org.junit.Assert.assertThrows;
+import android.content.ComponentName;
+
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -51,6 +53,38 @@
assertThat(request.getCredentialOptions().get(i)).isEqualTo(
expectedCredentialOptions.get(i));
}
+ assertThat(request.getPreferIdentityDocUi()).isFalse();
+ assertThat(request.preferImmediatelyAvailableCredentials()).isFalse();
+ assertThat(request.getPreferUiBrandingComponentName()).isNull();
+ }
+
+ @Test
+ public void constructor_nonDefaultPreferUiBrandingComponentName() {
+ ArrayList<CredentialOption> options = new ArrayList<>();
+ options.add(new GetPasswordOption());
+ ComponentName expectedComponentName = new ComponentName("test pkg", "test cls");
+
+ GetCredentialRequest request = new GetCredentialRequest(
+ options, /*origin=*/ null, /*preferIdentityDocUi=*/false, expectedComponentName);
+
+ assertThat(request.getCredentialOptions().get(0).isAutoSelectAllowed()).isFalse();
+ assertThat(request.getPreferUiBrandingComponentName()).isEqualTo(expectedComponentName);
+ }
+
+ @Test
+ public void constructor_nonDefaultPreferImmediatelyAvailableCredentials() {
+ ArrayList<CredentialOption> options = new ArrayList<>();
+ options.add(new GetPasswordOption());
+ boolean expectedPreferImmediatelyAvailableCredentials = true;
+
+ GetCredentialRequest request = new GetCredentialRequest(
+ options, /*origin=*/ null, /*preferIdentityDocUi=*/false,
+ /*preferUiBrandingComponentName=*/ null,
+ expectedPreferImmediatelyAvailableCredentials);
+
+ assertThat(request.getCredentialOptions().get(0).isAutoSelectAllowed()).isFalse();
+ assertThat(request.preferImmediatelyAvailableCredentials())
+ .isEqualTo(expectedPreferImmediatelyAvailableCredentials);
}
@Test
@@ -61,6 +95,7 @@
GetCredentialRequest request = new GetCredentialRequest(options);
assertThat(request.getCredentialOptions().get(0).isAutoSelectAllowed()).isFalse();
+ assertThat(request.getPreferIdentityDocUi()).isFalse();
}
@Test
@@ -96,6 +131,70 @@
assertThat(request.getCredentialOptions().get(i)).isEqualTo(
expectedCredentialOptions.get(i));
}
+ assertThat(request.getPreferIdentityDocUi()).isFalse();
+ assertThat(request.preferImmediatelyAvailableCredentials()).isFalse();
+ assertThat(request.getPreferUiBrandingComponentName()).isNull();
+ }
+
+ @Test
+ public void builder_setPreferIdentityDocUi() {
+ ArrayList<CredentialOption> expectedCredentialOptions = new ArrayList<>();
+ expectedCredentialOptions.add(new GetPasswordOption());
+ expectedCredentialOptions.add(new GetPublicKeyCredentialOption("json"));
+
+ GetCredentialRequest request = new GetCredentialRequest.Builder()
+ .setCredentialOptions(expectedCredentialOptions)
+ .setPreferIdentityDocUi(true)
+ .build();
+
+ assertThat(request.getCredentialOptions()).hasSize(expectedCredentialOptions.size());
+ for (int i = 0; i < expectedCredentialOptions.size(); i++) {
+ assertThat(request.getCredentialOptions().get(i)).isEqualTo(
+ expectedCredentialOptions.get(i));
+ }
+ assertThat(request.getPreferIdentityDocUi()).isTrue();
+ }
+
+ @Test
+ public void builder_setPreferImmediatelyAvailableCredentials() {
+ ArrayList<CredentialOption> expectedCredentialOptions = new ArrayList<>();
+ expectedCredentialOptions.add(new GetPasswordOption());
+ expectedCredentialOptions.add(new GetPublicKeyCredentialOption("json"));
+ boolean expectedPreferImmediatelyAvailableCredentials = true;
+
+ GetCredentialRequest request = new GetCredentialRequest.Builder()
+ .setCredentialOptions(expectedCredentialOptions)
+ .setPreferImmediatelyAvailableCredentials(
+ expectedPreferImmediatelyAvailableCredentials)
+ .build();
+
+ assertThat(request.getCredentialOptions()).hasSize(expectedCredentialOptions.size());
+ for (int i = 0; i < expectedCredentialOptions.size(); i++) {
+ assertThat(request.getCredentialOptions().get(i)).isEqualTo(
+ expectedCredentialOptions.get(i));
+ }
+ assertThat(request.preferImmediatelyAvailableCredentials())
+ .isEqualTo(expectedPreferImmediatelyAvailableCredentials);
+ }
+
+ @Test
+ public void builder_setPreferUiBrandingComponentName() {
+ ArrayList<CredentialOption> expectedCredentialOptions = new ArrayList<>();
+ expectedCredentialOptions.add(new GetPasswordOption());
+ expectedCredentialOptions.add(new GetPublicKeyCredentialOption("json"));
+ ComponentName expectedComponentName = new ComponentName("test pkg", "test cls");
+
+ GetCredentialRequest request = new GetCredentialRequest.Builder()
+ .setCredentialOptions(expectedCredentialOptions)
+ .setPreferUiBrandingComponentName(expectedComponentName)
+ .build();
+
+ assertThat(request.getCredentialOptions()).hasSize(expectedCredentialOptions.size());
+ for (int i = 0; i < expectedCredentialOptions.size(); i++) {
+ assertThat(request.getCredentialOptions().get(i)).isEqualTo(
+ expectedCredentialOptions.get(i));
+ }
+ assertThat(request.getPreferUiBrandingComponentName()).isEqualTo(expectedComponentName);
}
@Test
@@ -106,4 +205,30 @@
assertThat(request.getCredentialOptions().get(0).isAutoSelectAllowed()).isFalse();
}
+
+ @Test
+ public void frameworkConversion() {
+ ArrayList<CredentialOption> options = new ArrayList<>();
+ options.add(new GetPasswordOption());
+ boolean expectedPreferImmediatelyAvailableCredentials = true;
+ ComponentName expectedComponentName = new ComponentName("test pkg", "test cls");
+ boolean expectedPreferIdentityDocUi = true;
+ String expectedOrigin = "origin";
+ GetCredentialRequest request = new GetCredentialRequest(options, expectedOrigin,
+ expectedPreferIdentityDocUi, expectedComponentName,
+ expectedPreferImmediatelyAvailableCredentials);
+
+
+ GetCredentialRequest convertedRequest = GetCredentialRequest.createFrom(
+ options, request.getOrigin(), GetCredentialRequest.toRequestDataBundle(request)
+ );
+
+ assertThat(convertedRequest.getOrigin()).isEqualTo(expectedOrigin);
+ assertThat(convertedRequest.getPreferIdentityDocUi()).isEqualTo(
+ expectedPreferIdentityDocUi);
+ assertThat(convertedRequest.getPreferUiBrandingComponentName()).isEqualTo(
+ expectedComponentName);
+ assertThat(convertedRequest.preferImmediatelyAvailableCredentials()).isEqualTo(
+ expectedPreferImmediatelyAvailableCredentials);
+ }
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt
index fafee67..cfc3aeb 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt
@@ -16,13 +16,13 @@
package androidx.credentials
-import com.google.common.truth.Truth.assertThat
-
-import org.junit.Assert.assertThrows
-
+import android.content.ComponentName
+import androidx.credentials.GetCredentialRequest.Companion.createFrom
+import androidx.credentials.GetCredentialRequest.Companion.toRequestDataBundle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
@@ -56,6 +56,9 @@
)
}
assertThat(request.origin).isEqualTo(origin)
+ assertThat(request.preferIdentityDocUi).isFalse()
+ assertThat(request.preferImmediatelyAvailableCredentials).isFalse()
+ assertThat(request.preferUiBrandingComponentName).isNull()
}
@Test
@@ -68,6 +71,63 @@
assertThat(request.credentialOptions[0].isAutoSelectAllowed).isFalse()
assertThat(request.origin).isEqualTo(origin)
+ assertThat(request.preferIdentityDocUi).isFalse()
+ }
+
+ @Test
+ fun constructor_nonDefaultPreferUiBrandingComponentName() {
+ val options = java.util.ArrayList<CredentialOption>()
+ options.add(GetPasswordOption())
+ val expectedComponentName = ComponentName("test pkg", "test cls")
+
+ val request = GetCredentialRequest(
+ options, /*origin=*/null, /*preferIdentityDocUi=*/false, expectedComponentName
+ )
+
+ assertThat(request.credentialOptions[0].isAutoSelectAllowed).isFalse()
+ assertThat(request.preferUiBrandingComponentName).isEqualTo(expectedComponentName)
+ }
+
+ @Test
+ fun constructor_nonDefaultPreferImmediatelyAvailableCredentials() {
+ val options = java.util.ArrayList<CredentialOption>()
+ options.add(GetPasswordOption())
+ val expectedPreferImmediatelyAvailableCredentials = true
+
+ val request = GetCredentialRequest(
+ options,
+ origin = null,
+ preferIdentityDocUi = false,
+ preferUiBrandingComponentName = null,
+ expectedPreferImmediatelyAvailableCredentials
+ )
+
+ assertThat(request.credentialOptions[0].isAutoSelectAllowed).isFalse()
+ assertThat(request.preferImmediatelyAvailableCredentials)
+ .isEqualTo(expectedPreferImmediatelyAvailableCredentials)
+ }
+
+ @Test
+ fun builder_setPreferImmediatelyAvailableCredentials() {
+ val expectedCredentialOptions = java.util.ArrayList<CredentialOption>()
+ expectedCredentialOptions.add(GetPasswordOption())
+ expectedCredentialOptions.add(GetPublicKeyCredentialOption("json"))
+ val expectedPreferImmediatelyAvailableCredentials = true
+
+ val request = GetCredentialRequest.Builder()
+ .setCredentialOptions(expectedCredentialOptions)
+ .setPreferImmediatelyAvailableCredentials(
+ expectedPreferImmediatelyAvailableCredentials
+ ).build()
+
+ assertThat(request.credentialOptions).hasSize(expectedCredentialOptions.size)
+ for (i in expectedCredentialOptions.indices) {
+ assertThat(request.credentialOptions[i]).isEqualTo(
+ expectedCredentialOptions[i]
+ )
+ }
+ assertThat(request.preferImmediatelyAvailableCredentials)
+ .isEqualTo(expectedPreferImmediatelyAvailableCredentials)
}
@Test
@@ -105,9 +165,52 @@
expectedCredentialOptions[i]
)
}
+ assertThat(request.preferIdentityDocUi).isFalse()
+ assertThat(request.preferImmediatelyAvailableCredentials).isFalse()
+ assertThat(request.preferUiBrandingComponentName).isNull()
}
@Test
+ fun builder_setPreferIdentityDocUis() {
+ val expectedCredentialOptions = ArrayList<CredentialOption>()
+ expectedCredentialOptions.add(GetPasswordOption())
+ expectedCredentialOptions.add(GetPublicKeyCredentialOption("json"))
+
+ val request = GetCredentialRequest.Builder()
+ .setCredentialOptions(expectedCredentialOptions)
+ .setPreferIdentityDocUi(true)
+ .build()
+
+ assertThat(request.credentialOptions).hasSize(expectedCredentialOptions.size)
+ for (i in expectedCredentialOptions.indices) {
+ assertThat(request.credentialOptions[i]).isEqualTo(
+ expectedCredentialOptions[i]
+ )
+ }
+ assertThat(request.preferIdentityDocUi).isTrue()
+ }
+
+ @Test
+ fun builder_setPreferUiBrandingComponentName() {
+ val expectedCredentialOptions = java.util.ArrayList<CredentialOption>()
+ expectedCredentialOptions.add(GetPasswordOption())
+ expectedCredentialOptions.add(GetPublicKeyCredentialOption("json"))
+ val expectedComponentName = ComponentName("test pkg", "test cls")
+
+ val request = GetCredentialRequest.Builder()
+ .setCredentialOptions(expectedCredentialOptions)
+ .setPreferUiBrandingComponentName(expectedComponentName)
+ .build()
+
+ assertThat(request.credentialOptions).hasSize(expectedCredentialOptions.size)
+ for (i in expectedCredentialOptions.indices) {
+ assertThat(request.credentialOptions[i]).isEqualTo(
+ expectedCredentialOptions[i]
+ )
+ }
+ assertThat(request.preferUiBrandingComponentName).isEqualTo(expectedComponentName)
+ }
+ @Test
fun builder_defaultAutoSelect() {
val request = GetCredentialRequest.Builder()
.addCredentialOption(GetPasswordOption())
@@ -115,4 +218,34 @@
assertThat(request.credentialOptions[0].isAutoSelectAllowed).isFalse()
}
+
+ @Test
+ fun frameworkConversion() {
+ val options = java.util.ArrayList<CredentialOption>()
+ options.add(GetPasswordOption())
+ val expectedPreferImmediatelyAvailableCredentials = true
+ val expectedComponentName = ComponentName("test pkg", "test cls")
+ val expectedPreferIdentityDocUi = true
+ val expectedOrigin = "origin"
+ val request = GetCredentialRequest(
+ options, expectedOrigin,
+ expectedPreferIdentityDocUi, expectedComponentName,
+ expectedPreferImmediatelyAvailableCredentials
+ )
+
+ val convertedRequest = createFrom(
+ options, request.origin, toRequestDataBundle(request)
+ )
+
+ assertThat(convertedRequest.origin).isEqualTo(expectedOrigin)
+ assertThat(convertedRequest.preferIdentityDocUi).isEqualTo(
+ expectedPreferIdentityDocUi
+ )
+ assertThat(convertedRequest.preferUiBrandingComponentName).isEqualTo(
+ expectedComponentName
+ )
+ assertThat(convertedRequest.preferImmediatelyAvailableCredentials).isEqualTo(
+ expectedPreferImmediatelyAvailableCredentials
+ )
+ }
}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionJavaTest.java
index 4d6de2d..b2c0494 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionJavaTest.java
@@ -20,14 +20,19 @@
import static org.junit.Assert.assertThrows;
+import android.content.ComponentName;
import android.os.Bundle;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.google.common.collect.ImmutableSet;
+
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Set;
+
@RunWith(AndroidJUnit4.class)
@SmallTest
public class GetCustomCredentialOptionJavaTest {
@@ -74,12 +79,17 @@
expectedCandidateQueryDataBundle.putBoolean("key", true);
boolean expectedSystemProvider = true;
boolean expectedAutoSelectAllowed = false;
+ Set<ComponentName> expectedAllowedProviders = ImmutableSet.of(
+ new ComponentName("pkg", "cls"),
+ new ComponentName("pkg2", "cls2")
+ );
GetCustomCredentialOption option = new GetCustomCredentialOption(expectedType,
expectedBundle,
expectedCandidateQueryDataBundle,
expectedSystemProvider,
- expectedAutoSelectAllowed);
+ expectedAutoSelectAllowed,
+ expectedAllowedProviders);
assertThat(option.getType()).isEqualTo(expectedType);
assertThat(TestUtilsKt.equals(option.getRequestData(), expectedBundle)).isTrue();
@@ -87,5 +97,43 @@
expectedCandidateQueryDataBundle)).isTrue();
assertThat(option.isAutoSelectAllowed()).isEqualTo(expectedAutoSelectAllowed);
assertThat(option.isSystemProviderRequired()).isEqualTo(expectedSystemProvider);
+ assertThat(option.getAllowedProviders())
+ .containsAtLeastElementsIn(expectedAllowedProviders);
+ }
+
+ @Test
+ public void frameworkConversion_success() {
+ String expectedType = "TYPE";
+ Bundle expectedBundle = new Bundle();
+ expectedBundle.putString("Test", "Test");
+ Bundle expectedCandidateQueryDataBundle = new Bundle();
+ expectedCandidateQueryDataBundle.putBoolean("key", true);
+ boolean expectedSystemProvider = true;
+ boolean expectedAutoSelectAllowed = false;
+ Set<ComponentName> expectedAllowedProviders = ImmutableSet.of(
+ new ComponentName("pkg", "cls"),
+ new ComponentName("pkg2", "cls2")
+ );
+ GetCustomCredentialOption option = new GetCustomCredentialOption(expectedType,
+ expectedBundle,
+ expectedCandidateQueryDataBundle,
+ expectedSystemProvider,
+ expectedAutoSelectAllowed,
+ expectedAllowedProviders);
+
+ CredentialOption convertedOption = CredentialOption.createFrom(
+ option.getType(), option.getRequestData(), option.getCandidateQueryData(),
+ option.isSystemProviderRequired(), option.getAllowedProviders());
+
+ assertThat(convertedOption).isInstanceOf(GetCustomCredentialOption.class);
+ GetCustomCredentialOption actualOption = (GetCustomCredentialOption) convertedOption;
+ assertThat(actualOption.getType()).isEqualTo(expectedType);
+ assertThat(TestUtilsKt.equals(actualOption.getRequestData(), expectedBundle)).isTrue();
+ assertThat(TestUtilsKt.equals(actualOption.getCandidateQueryData(),
+ expectedCandidateQueryDataBundle)).isTrue();
+ assertThat(actualOption.isAutoSelectAllowed()).isEqualTo(expectedAutoSelectAllowed);
+ assertThat(actualOption.isSystemProviderRequired()).isEqualTo(expectedSystemProvider);
+ assertThat(actualOption.getAllowedProviders())
+ .containsAtLeastElementsIn(expectedAllowedProviders);
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionTest.kt
index 4910a54..8a3c0828 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionTest.kt
@@ -16,7 +16,9 @@
package androidx.credentials
+import android.content.ComponentName
import android.os.Bundle
+import androidx.credentials.CredentialOption.Companion.createFrom
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
@@ -56,15 +58,20 @@
expectedBundle.putString("Test", "Test")
val expectedCandidateQueryDataBundle = Bundle()
expectedCandidateQueryDataBundle.putBoolean("key", true)
- val expectedAutoSelectAllowed = false
+ val expectedAutoSelectAllowed = true
val expectedSystemProvider = true
+ val expectedAllowedProviders: Set<ComponentName> = setOf(
+ ComponentName("pkg", "cls"),
+ ComponentName("pkg2", "cls2")
+ )
val option = GetCustomCredentialOption(
expectedType,
expectedBundle,
expectedCandidateQueryDataBundle,
expectedSystemProvider,
- expectedAutoSelectAllowed
+ expectedAutoSelectAllowed,
+ expectedAllowedProviders
)
assertThat(option.type).isEqualTo(expectedType)
@@ -75,6 +82,52 @@
expectedCandidateQueryDataBundle
)
).isTrue()
+ assertThat(option.isAutoSelectAllowed).isEqualTo(expectedAutoSelectAllowed)
assertThat(option.isSystemProviderRequired).isEqualTo(expectedSystemProvider)
+ assertThat(option.allowedProviders)
+ .containsAtLeastElementsIn(expectedAllowedProviders)
+ }
+
+ @Test
+ fun frameworkConversion_success() {
+ val expectedType = "TYPE"
+ val expectedBundle = Bundle()
+ expectedBundle.putString("Test", "Test")
+ val expectedCandidateQueryDataBundle = Bundle()
+ expectedCandidateQueryDataBundle.putBoolean("key", true)
+ val expectedSystemProvider = true
+ val expectedAutoSelectAllowed = false
+ val expectedAllowedProviders: Set<ComponentName> = setOf(
+ ComponentName("pkg", "cls"),
+ ComponentName("pkg2", "cls2")
+ )
+ val option = GetCustomCredentialOption(
+ expectedType,
+ expectedBundle,
+ expectedCandidateQueryDataBundle,
+ expectedSystemProvider,
+ expectedAutoSelectAllowed,
+ expectedAllowedProviders
+ )
+
+ val convertedOption = createFrom(
+ option.type, option.requestData, option.candidateQueryData,
+ option.isSystemProviderRequired, option.allowedProviders
+ )
+
+ assertThat(convertedOption).isInstanceOf(GetCustomCredentialOption::class.java)
+ val actualOption = convertedOption as GetCustomCredentialOption
+ assertThat(actualOption.type).isEqualTo(expectedType)
+ assertThat(equals(actualOption.requestData, expectedBundle)).isTrue()
+ assertThat(
+ equals(
+ actualOption.candidateQueryData,
+ expectedCandidateQueryDataBundle
+ )
+ ).isTrue()
+ assertThat(actualOption.isAutoSelectAllowed).isEqualTo(expectedAutoSelectAllowed)
+ assertThat(actualOption.isSystemProviderRequired).isEqualTo(expectedSystemProvider)
+ assertThat(actualOption.allowedProviders)
+ .containsAtLeastElementsIn(expectedAllowedProviders)
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionJavaTest.java
index b988bb3..16e278d 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionJavaTest.java
@@ -18,38 +18,98 @@
import static com.google.common.truth.Truth.assertThat;
-import android.os.Bundle;
+import android.content.ComponentName;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.google.common.collect.ImmutableSet;
+
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Set;
+
@RunWith(AndroidJUnit4.class)
@SmallTest
public class GetPasswordOptionJavaTest {
@Test
- public void getter_frameworkProperties() {
+ public void emptyConstructor_success() {
GetPasswordOption option = new GetPasswordOption();
- Bundle expectedRequestDataBundle = new Bundle();
- expectedRequestDataBundle.putBoolean(GetPasswordOption.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
- false);
+
+ assertThat(option.isAutoSelectAllowed()).isFalse();
+ assertThat(option.getAllowedUserIds()).isEmpty();
+ assertThat(option.getAllowedProviders()).isEmpty();
+ }
+
+ @Test
+ public void construction_setOptionalValues_success() {
+ boolean expectedIsAutoSelectAllowed = true;
+ Set<String> expectedAllowedUserIds = ImmutableSet.of("id1", "id2", "id3");
+ Set<ComponentName> expectedAllowedProviders = ImmutableSet.of(
+ new ComponentName("pkg", "cls"),
+ new ComponentName("pkg2", "cls2")
+ );
+
+ GetPasswordOption option = new GetPasswordOption(
+ expectedAllowedUserIds, expectedIsAutoSelectAllowed,
+ expectedAllowedProviders);
+
+ assertThat(option.isAutoSelectAllowed()).isEqualTo(expectedIsAutoSelectAllowed);
+ assertThat(option.getAllowedUserIds()).containsExactlyElementsIn(expectedAllowedUserIds);
+ assertThat(option.getAllowedProviders())
+ .containsExactlyElementsIn(expectedAllowedProviders);
+ }
+
+ @Test
+ public void getter_frameworkProperties() {
+ Set<String> expectedAllowedUserIds = ImmutableSet.of("id1", "id2", "id3");
+ Set<ComponentName> expectedAllowedProviders = ImmutableSet.of(
+ new ComponentName("pkg", "cls"),
+ new ComponentName("pkg2", "cls2")
+ );
+ boolean expectedIsAutoSelectAllowed = true;
+
+ GetPasswordOption option = new GetPasswordOption(expectedAllowedUserIds,
+ expectedIsAutoSelectAllowed, expectedAllowedProviders);
assertThat(option.getType()).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL);
- assertThat(TestUtilsKt.equals(option.getRequestData(), expectedRequestDataBundle)).isTrue();
- assertThat(TestUtilsKt.equals(option.getCandidateQueryData(), Bundle.EMPTY)).isTrue();
+ assertThat(option.getRequestData().getBoolean(
+ CredentialOption.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED)).isTrue();
+ assertThat(option.getRequestData().getStringArrayList(
+ GetPasswordOption.BUNDLE_KEY_ALLOWED_USER_IDS))
+ .containsExactlyElementsIn(expectedAllowedUserIds);
+ assertThat(option.getCandidateQueryData().getBoolean(
+ CredentialOption.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED)).isTrue();
+ assertThat(option.getCandidateQueryData().getStringArrayList(
+ GetPasswordOption.BUNDLE_KEY_ALLOWED_USER_IDS))
+ .containsExactlyElementsIn(expectedAllowedUserIds);
assertThat(option.isSystemProviderRequired()).isFalse();
+ assertThat(option.getAllowedProviders())
+ .containsExactlyElementsIn(expectedAllowedProviders);
}
@Test
public void frameworkConversion_success() {
- GetPasswordOption option = new GetPasswordOption();
+ boolean expectedIsAutoSelectAllowed = true;
+ Set<ComponentName> expectedAllowedProviders = ImmutableSet.of(
+ new ComponentName("pkg", "cls"),
+ new ComponentName("pkg2", "cls2")
+ );
+ Set<String> expectedAllowedUserIds = ImmutableSet.of("id1", "id2", "id3");
+ GetPasswordOption option = new GetPasswordOption(expectedAllowedUserIds,
+ expectedIsAutoSelectAllowed, expectedAllowedProviders);
CredentialOption convertedOption = CredentialOption.createFrom(
option.getType(), option.getRequestData(), option.getCandidateQueryData(),
- option.isSystemProviderRequired());
+ option.isSystemProviderRequired(),
+ option.getAllowedProviders());
assertThat(convertedOption).isInstanceOf(GetPasswordOption.class);
+ GetPasswordOption getPasswordOption = (GetPasswordOption) convertedOption;
+ assertThat(getPasswordOption.isAutoSelectAllowed()).isEqualTo(expectedIsAutoSelectAllowed);
+ assertThat(getPasswordOption.getAllowedProviders())
+ .containsExactlyElementsIn(expectedAllowedProviders);
+ assertThat(option.getAllowedUserIds()).containsExactlyElementsIn(expectedAllowedUserIds);
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionTest.kt
index 2a8c6d7..9af55946 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionTest.kt
@@ -16,8 +16,9 @@
package androidx.credentials
-import android.os.Bundle
+import android.content.ComponentName
import androidx.credentials.CredentialOption.Companion.createFrom
+import androidx.credentials.GetPasswordOption.Companion.BUNDLE_KEY_ALLOWED_USER_IDS
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
@@ -28,31 +29,92 @@
@SmallTest
class GetPasswordOptionTest {
@Test
- fun getter_frameworkProperties() {
+ fun emptyConstructor_success() {
val option = GetPasswordOption()
- val expectedRequestDataBundle = Bundle()
- expectedRequestDataBundle.putBoolean(
- CredentialOption.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
- false
+
+ assertThat(option.isAutoSelectAllowed).isFalse()
+ assertThat(option.allowedProviders).isEmpty()
+ assertThat(option.allowedUserIds).isEmpty()
+ }
+
+ @Test
+ fun construction_setOptionalValues_success() {
+ val expectedIsAutoSelectAllowed = true
+ val expectedAllowedProviders: Set<ComponentName> = setOf(
+ ComponentName("pkg", "cls"),
+ ComponentName("pkg2", "cls2")
+ )
+ val expectedAllowedUserIds: Set<String> = setOf("id1", "id2", "id3")
+
+ val option = GetPasswordOption(
+ allowedUserIds = expectedAllowedUserIds,
+ isAutoSelectAllowed = expectedIsAutoSelectAllowed,
+ allowedProviders = expectedAllowedProviders,
+ )
+
+ assertThat(option.isAutoSelectAllowed).isEqualTo(expectedIsAutoSelectAllowed)
+ assertThat(option.allowedProviders)
+ .containsExactlyElementsIn(expectedAllowedProviders)
+ assertThat(option.allowedUserIds)
+ .containsExactlyElementsIn(expectedAllowedUserIds)
+ }
+
+ @Test
+ fun getter_frameworkProperties() {
+ val expectedAllowedUserIds: Set<String> = setOf("id1", "id2", "id3")
+ val expectedAllowedProviders: Set<ComponentName> = setOf(
+ ComponentName("pkg", "cls"),
+ ComponentName("pkg2", "cls2")
+ )
+ val expectedIsAutoSelectAllowed = true
+
+ val option = GetPasswordOption(
+ allowedUserIds = expectedAllowedUserIds,
+ isAutoSelectAllowed = expectedIsAutoSelectAllowed,
+ allowedProviders = expectedAllowedProviders,
)
assertThat(option.type).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL)
- assertThat(equals(option.requestData, expectedRequestDataBundle)).isTrue()
- assertThat(equals(option.candidateQueryData, Bundle.EMPTY)).isTrue()
+ assertThat(option.requestData.getBoolean(
+ CredentialOption.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED)).isTrue()
+ assertThat(option.requestData.getStringArrayList(
+ BUNDLE_KEY_ALLOWED_USER_IDS)).containsExactlyElementsIn(expectedAllowedUserIds)
+ assertThat(option.candidateQueryData.getBoolean(
+ CredentialOption.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED)).isTrue()
+ assertThat(option.candidateQueryData.getStringArrayList(
+ BUNDLE_KEY_ALLOWED_USER_IDS)).containsExactlyElementsIn(expectedAllowedUserIds)
assertThat(option.isSystemProviderRequired).isFalse()
+ assertThat(option.allowedProviders)
+ .containsExactlyElementsIn(expectedAllowedProviders)
}
@Test
fun frameworkConversion_success() {
- val option = GetPasswordOption()
+ val expectedAllowedUserIds: Set<String> = setOf("id1", "id2", "id3")
+ val expectedAutoSelectAllowed = true
+ val expectedAllowedProviders: Set<ComponentName> = setOf(
+ ComponentName("pkg", "cls"),
+ ComponentName("pkg2", "cls2")
+ )
+ val option = GetPasswordOption(
+ allowedUserIds = expectedAllowedUserIds,
+ isAutoSelectAllowed = expectedAutoSelectAllowed,
+ allowedProviders = expectedAllowedProviders,
+ )
val convertedOption = createFrom(
option.type,
option.requestData,
option.candidateQueryData,
- option.isSystemProviderRequired
+ option.isSystemProviderRequired,
+ option.allowedProviders
)
assertThat(convertedOption).isInstanceOf(GetPasswordOption::class.java)
+ assertThat(convertedOption.isAutoSelectAllowed).isEqualTo(expectedAutoSelectAllowed)
+ assertThat(convertedOption.allowedProviders)
+ .containsExactlyElementsIn(expectedAllowedProviders)
+ assertThat((convertedOption as GetPasswordOption).allowedUserIds)
+ .containsExactlyElementsIn(expectedAllowedUserIds)
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java
index da4e145..42220a4 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java
@@ -17,21 +17,25 @@
package androidx.credentials;
import static androidx.credentials.CredentialOption.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED;
-import static androidx.credentials.GetPublicKeyCredentialOption.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS;
import static androidx.credentials.GetPublicKeyCredentialOption.BUNDLE_KEY_REQUEST_JSON;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
+import android.content.ComponentName;
import android.os.Bundle;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.google.common.collect.ImmutableSet;
+
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Set;
+
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -60,29 +64,6 @@
}
@Test
- public void constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
- GetPublicKeyCredentialOption getPublicKeyCredentialOpt =
- new GetPublicKeyCredentialOption(
- "JSON");
- boolean preferImmediatelyAvailableCredentialsActual =
- getPublicKeyCredentialOpt.preferImmediatelyAvailableCredentials();
- assertThat(preferImmediatelyAvailableCredentialsActual).isFalse();
- }
-
- @Test
- public void constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
- boolean preferImmediatelyAvailableCredentialsExpected = true;
- String clientDataHash = "hash";
- GetPublicKeyCredentialOption getPublicKeyCredentialOpt =
- new GetPublicKeyCredentialOption(
- "JSON", clientDataHash, preferImmediatelyAvailableCredentialsExpected);
- boolean preferImmediatelyAvailableCredentialsActual =
- getPublicKeyCredentialOpt.preferImmediatelyAvailableCredentials();
- assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
- preferImmediatelyAvailableCredentialsExpected);
- }
-
- @Test
public void getter_requestJson_success() {
String testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
GetPublicKeyCredentialOption getPublicKeyCredentialOpt =
@@ -93,47 +74,53 @@
@Test
public void getter_frameworkProperties_success() {
+ Set<ComponentName> expectedAllowedProviders = ImmutableSet.of(
+ new ComponentName("pkg", "cls"),
+ new ComponentName("pkg2", "cls2")
+ );
String requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
- boolean preferImmediatelyAvailableCredentialsExpected = false;
boolean expectedIsAutoSelect = true;
- String clientDataHash = "hash";
+ byte[] clientDataHash = "hash".getBytes();
Bundle expectedData = new Bundle();
expectedData.putString(
PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
GetPublicKeyCredentialOption.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION);
expectedData.putString(BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
- expectedData.putString(GetPublicKeyCredentialOption.BUNDLE_KEY_CLIENT_DATA_HASH,
+ expectedData.putByteArray(GetPublicKeyCredentialOption.BUNDLE_KEY_CLIENT_DATA_HASH,
clientDataHash);
- expectedData.putBoolean(
- BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
- preferImmediatelyAvailableCredentialsExpected);
expectedData.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, expectedIsAutoSelect);
GetPublicKeyCredentialOption option = new GetPublicKeyCredentialOption(
- requestJsonExpected, clientDataHash, preferImmediatelyAvailableCredentialsExpected);
+ requestJsonExpected, clientDataHash, expectedAllowedProviders);
assertThat(option.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
assertThat(TestUtilsKt.equals(option.getRequestData(), expectedData)).isTrue();
- expectedData.remove(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED);
assertThat(TestUtilsKt.equals(option.getCandidateQueryData(), expectedData)).isTrue();
assertThat(option.isSystemProviderRequired()).isFalse();
+ assertThat(option.getAllowedProviders())
+ .containsAtLeastElementsIn(expectedAllowedProviders);
}
@Test
public void frameworkConversion_success() {
- String clientDataHash = "hash";
- GetPublicKeyCredentialOption option =
- new GetPublicKeyCredentialOption("json", clientDataHash, true);
+ byte[] clientDataHash = "hash".getBytes();
+ Set<ComponentName> expectedAllowedProviders = ImmutableSet.of(
+ new ComponentName("pkg", "cls"),
+ new ComponentName("pkg2", "cls2")
+ );
+ GetPublicKeyCredentialOption option = new GetPublicKeyCredentialOption(
+ "json", clientDataHash, expectedAllowedProviders);
CredentialOption convertedOption = CredentialOption.createFrom(
option.getType(), option.getRequestData(),
- option.getCandidateQueryData(), option.isSystemProviderRequired());
+ option.getCandidateQueryData(), option.isSystemProviderRequired(),
+ option.getAllowedProviders());
assertThat(convertedOption).isInstanceOf(GetPublicKeyCredentialOption.class);
GetPublicKeyCredentialOption convertedSubclassOption =
(GetPublicKeyCredentialOption) convertedOption;
assertThat(convertedSubclassOption.getRequestJson()).isEqualTo(option.getRequestJson());
- assertThat(convertedSubclassOption.preferImmediatelyAvailableCredentials()).isEqualTo(
- option.preferImmediatelyAvailableCredentials());
+ assertThat(convertedSubclassOption.getAllowedProviders())
+ .containsAtLeastElementsIn(expectedAllowedProviders);
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
index a4648fa..a270c7a4 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
@@ -16,6 +16,7 @@
package androidx.credentials
+import android.content.ComponentName
import android.os.Bundle
import androidx.credentials.CredentialOption.Companion.createFrom
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -45,29 +46,6 @@
}
@Test
- fun constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
- val getPublicKeyCredentialOpt = GetPublicKeyCredentialOption(
- "JSON"
- )
- val preferImmediatelyAvailableCredentialsActual =
- getPublicKeyCredentialOpt.preferImmediatelyAvailableCredentials
- assertThat(preferImmediatelyAvailableCredentialsActual).isFalse()
- }
-
- @Test
- fun constructor_setPreferImmediatelyAvailableCredentialsTrue() {
- val preferImmediatelyAvailableCredentialsExpected = true
- val clientDataHash = "hash"
- val getPublicKeyCredentialOpt = GetPublicKeyCredentialOption(
- "JSON", clientDataHash, preferImmediatelyAvailableCredentialsExpected
- )
- val preferImmediatelyAvailableCredentialsActual =
- getPublicKeyCredentialOpt.preferImmediatelyAvailableCredentials
- assertThat(preferImmediatelyAvailableCredentialsActual)
- .isEqualTo(preferImmediatelyAvailableCredentialsExpected)
- }
-
- @Test
fun getter_requestJson_success() {
val testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
val createPublicKeyCredentialReq = GetPublicKeyCredentialOption(testJsonExpected)
@@ -78,9 +56,12 @@
@Test
fun getter_frameworkProperties_success() {
val requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
- val preferImmediatelyAvailableCredentialsExpected = false
val expectedAutoSelectAllowed = true
- val clientDataHash = "hash"
+ val expectedAllowedProviders: Set<ComponentName> = setOf(
+ ComponentName("pkg", "cls"),
+ ComponentName("pkg2", "cls2")
+ )
+ val clientDataHash = "hash".toByteArray()
val expectedData = Bundle()
expectedData.putString(
PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
@@ -90,39 +71,41 @@
GetPublicKeyCredentialOption.BUNDLE_KEY_REQUEST_JSON,
requestJsonExpected
)
- expectedData.putString(GetPublicKeyCredentialOption.BUNDLE_KEY_CLIENT_DATA_HASH,
+ expectedData.putByteArray(GetPublicKeyCredentialOption.BUNDLE_KEY_CLIENT_DATA_HASH,
clientDataHash)
expectedData.putBoolean(
- GetPublicKeyCredentialOption.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
- preferImmediatelyAvailableCredentialsExpected
- )
- expectedData.putBoolean(
CredentialOption.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
expectedAutoSelectAllowed
)
val option = GetPublicKeyCredentialOption(
- requestJsonExpected, clientDataHash, preferImmediatelyAvailableCredentialsExpected
+ requestJsonExpected, clientDataHash, expectedAllowedProviders
)
assertThat(option.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
assertThat(equals(option.requestData, expectedData)).isTrue()
- expectedData.remove(CredentialOption.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED)
assertThat(equals(option.candidateQueryData, expectedData)).isTrue()
assertThat(option.isSystemProviderRequired).isFalse()
assertThat(option.isAutoSelectAllowed).isTrue()
+ assertThat(option.allowedProviders).containsAtLeastElementsIn(expectedAllowedProviders)
}
@Test
fun frameworkConversion_success() {
- val clientDataHash = "hash"
- val option = GetPublicKeyCredentialOption("json", clientDataHash, true)
+ val clientDataHash = "hash".toByteArray()
+ val expectedAllowedProviders: Set<ComponentName> = setOf(
+ ComponentName("pkg", "cls"),
+ ComponentName("pkg2", "cls2")
+ )
+ val option = GetPublicKeyCredentialOption(
+ "json", clientDataHash, expectedAllowedProviders)
val convertedOption = createFrom(
option.type,
option.requestData,
option.candidateQueryData,
- option.isSystemProviderRequired
+ option.isSystemProviderRequired,
+ option.allowedProviders,
)
assertThat(convertedOption).isInstanceOf(
@@ -130,7 +113,7 @@
)
val convertedSubclassOption = convertedOption as GetPublicKeyCredentialOption
assertThat(convertedSubclassOption.requestJson).isEqualTo(option.requestJson)
- assertThat(convertedSubclassOption.preferImmediatelyAvailableCredentials)
- .isEqualTo(option.preferImmediatelyAvailableCredentials)
+ assertThat(convertedOption.allowedProviders)
+ .containsAtLeastElementsIn(expectedAllowedProviders)
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialJavaTest.java
index 659dd33..3bc767a 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialJavaTest.java
@@ -31,6 +31,13 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class PasswordCredentialJavaTest {
+
+ @Test
+ public void typeConstant() {
+ assertThat(PasswordCredential.TYPE_PASSWORD_CREDENTIAL)
+ .isEqualTo("android.credentials.TYPE_PASSWORD_CREDENTIAL");
+ }
+
@Test
public void constructor_nullId_throws() {
assertThrows(
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialTest.kt
index 2c3f59d..7530833 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialTest.kt
@@ -28,6 +28,13 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
class PasswordCredentialTest {
+
+ @Test
+ fun typeConstant() {
+ assertThat(PasswordCredential.TYPE_PASSWORD_CREDENTIAL)
+ .isEqualTo("android.credentials.TYPE_PASSWORD_CREDENTIAL")
+ }
+
@Test
fun constructor_emptyPassword_throws() {
assertThrows<IllegalArgumentException> {
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialJavaTest.java
index 3a63354..6fd3cd1 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialJavaTest.java
@@ -36,6 +36,12 @@
public class PublicKeyCredentialJavaTest {
@Test
+ public void typeConstant() {
+ assertThat(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
+ .isEqualTo("androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL");
+ }
+
+ @Test
public void constructor_emptyJson_throwsIllegalArgumentException() {
assertThrows("Expected empty Json to throw IllegalArgumentException",
IllegalArgumentException.class,
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialTest.kt
index d701cae..17ff76c 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialTest.kt
@@ -33,6 +33,12 @@
class PublicKeyCredentialTest {
@Test
+ fun typeConstant() {
+ assertThat(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
+ .isEqualTo("androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL")
+ }
+
+ @Test
fun constructor_emptyJson_throwsIllegalArgumentException() {
Assert.assertThrows(
"Expected empty Json to throw IllegalArgumentException",
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
index 440f3db..a69a475 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
@@ -16,8 +16,12 @@
package androidx.credentials
+import android.graphics.drawable.Icon
import android.os.Build
import android.os.Bundle
+import android.service.credentials.CallingAppInfo
+import androidx.annotation.RequiresApi
+import androidx.core.os.BuildCompat
/** True if the two Bundles contain the same elements, and false otherwise. */
@Suppress("DEPRECATION")
@@ -68,7 +72,15 @@
/** True if the device running the test is post framework api level,
* false if pre framework api level. */
fun isPostFrameworkApiLevel(): Boolean {
- return !((Build.VERSION.SDK_INT <= MAX_CRED_MAN_PRE_FRAMEWORK_API_LEVEL) &&
- !(Build.VERSION.SDK_INT == MAX_CRED_MAN_PRE_FRAMEWORK_API_LEVEL &&
- Build.VERSION.PREVIEW_SDK_INT > 0))
+ return BuildCompat.isAtLeastU()
+}
+
+@RequiresApi(Build.VERSION_CODES.P)
+fun equals(a: Icon, b: Icon): Boolean {
+ return a.type == b.type && a.resId == b.resId
+}
+
+@RequiresApi(34)
+fun equals(a: CallingAppInfo, b: CallingAppInfo): Boolean {
+ return a.packageName == b.packageName && a.origin == b.origin
}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreateCredentialResponseJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreateCredentialResponseJavaTest.java
new file mode 100644
index 0000000..5373fb5
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreateCredentialResponseJavaTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.credentials.provider;
+
+import static androidx.credentials.provider.ui.UiUtils.constructCreateEntryWithSimpleParams;
+import static androidx.credentials.provider.ui.UiUtils.constructRemoteEntry;
+import static androidx.credentials.provider.ui.UiUtils.constructRemoteEntryDefault;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.core.os.BuildCompat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BeginCreateCredentialResponseJavaTest {
+
+ @Test
+ public void constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ new BeginCreateCredentialResponse(
+ Arrays.asList(constructCreateEntryWithSimpleParams("AccountName",
+ "Desc")),
+ null
+ );
+ }
+
+ @Test
+ public void builder_createEntriesOnly_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ new BeginCreateCredentialResponse.Builder().setCreateEntries(
+ Arrays.asList(constructCreateEntryWithSimpleParams("AccountName",
+ "Desc"))
+ ).build();
+ }
+
+ @Test
+ public void builder_remoteEntryOnly_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ new BeginCreateCredentialResponse.Builder().setRemoteEntry(
+ constructRemoteEntry()
+ ).build();
+ }
+
+ @Test
+ public void constructor_nullList_throws() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ assertThrows("Expected null list to throw NPE",
+ NullPointerException.class,
+ () -> new BeginCreateCredentialResponse(
+ null, null)
+ );
+ }
+
+ @Test
+ public void buildConstruct_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ new BeginCreateCredentialResponse.Builder().setCreateEntries(
+ Arrays.asList(constructCreateEntryWithSimpleParams("AccountName",
+ "Desc"))).build();
+ }
+
+ @Test
+ public void buildConstruct_nullList_throws() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ assertThrows("Expected null list to throw NPE",
+ NullPointerException.class,
+ () -> new BeginCreateCredentialResponse.Builder().setCreateEntries(null).build()
+ );
+ }
+
+ @Test
+ public void getter_createEntry() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ String expectedAccountName = "AccountName";
+ String expectedDescription = "Desc";
+
+ BeginCreateCredentialResponse response = new BeginCreateCredentialResponse(
+ Collections.singletonList(constructCreateEntryWithSimpleParams(expectedAccountName,
+ expectedDescription)), null);
+ String actualAccountName = response.getCreateEntries().get(0).getAccountName().toString();
+ String actualDescription = response.getCreateEntries().get(0).getDescription().toString();
+
+ assertThat(actualAccountName).isEqualTo(expectedAccountName);
+ assertThat(actualDescription).isEqualTo(expectedDescription);
+ }
+
+ @Test
+ public void getter_remoteEntry_null() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ RemoteEntry expectedRemoteEntry = null;
+ BeginCreateCredentialResponse response = new BeginCreateCredentialResponse(
+ Arrays.asList(constructCreateEntryWithSimpleParams("AccountName",
+ "Desc")),
+ expectedRemoteEntry
+ );
+ RemoteEntry actualRemoteEntry = response.getRemoteEntry();
+
+ assertThat(actualRemoteEntry).isEqualTo(expectedRemoteEntry);
+ }
+
+ @Test
+ public void getter_remoteEntry_nonNull() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ RemoteEntry expectedRemoteEntry = constructRemoteEntryDefault();
+
+ BeginCreateCredentialResponse response = new BeginCreateCredentialResponse(
+ Arrays.asList(constructCreateEntryWithSimpleParams("AccountName",
+ "Desc")),
+ expectedRemoteEntry
+ );
+ RemoteEntry actualRemoteEntry = response.getRemoteEntry();
+
+ assertThat(actualRemoteEntry).isEqualTo(expectedRemoteEntry);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreateCredentialResponseTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreateCredentialResponseTest.kt
new file mode 100644
index 0000000..675a6c9
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreateCredentialResponseTest.kt
@@ -0,0 +1,144 @@
+/*
+ * 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.credentials.provider
+
+import androidx.core.os.BuildCompat
+import androidx.credentials.provider.ui.UiUtils.Companion.constructCreateEntryWithSimpleParams
+import androidx.credentials.provider.ui.UiUtils.Companion.constructRemoteEntryDefault
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class BeginCreateCredentialResponseTest {
+
+ @Test
+ fun constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+
+ BeginCreateCredentialResponse(
+ createEntries = listOf(
+ constructCreateEntryWithSimpleParams(
+ "AccountName",
+ "Desc"
+ )
+ ),
+ remoteEntry = constructRemoteEntryDefault()
+ )
+ }
+
+ @Test
+ fun constructor_createEntriesOnly() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+
+ BeginCreateCredentialResponse(
+ createEntries = listOf(
+ constructCreateEntryWithSimpleParams(
+ "AccountName",
+ "Desc"
+ )
+ )
+ )
+ }
+
+ @Test
+ fun constructor_remoteEntryOnly() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+
+ BeginCreateCredentialResponse(
+ remoteEntry = constructRemoteEntryDefault()
+ )
+ }
+
+ @Test
+ fun getter_createEntry() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val expectedAccountName = "AccountName"
+ val expectedDescription = "Desc"
+ val expectedSize = 1
+
+ val beginCreateCredentialResponse = BeginCreateCredentialResponse(
+ listOf(
+ constructCreateEntryWithSimpleParams(
+ expectedAccountName,
+ expectedDescription
+ )
+ ), null
+ )
+ val actualAccountName = beginCreateCredentialResponse.createEntries[0].accountName
+ val actualDescription = beginCreateCredentialResponse.createEntries[0].description
+
+ assertThat(beginCreateCredentialResponse.createEntries.size).isEqualTo(expectedSize)
+ assertThat(actualAccountName).isEqualTo(expectedAccountName)
+ assertThat(actualDescription).isEqualTo(expectedDescription)
+ }
+
+ @Test
+ fun getter_remoteEntry_null() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+
+ val expectedRemoteEntry: RemoteEntry? = null
+ val beginCreateCredentialResponse = BeginCreateCredentialResponse(
+ listOf(
+ constructCreateEntryWithSimpleParams(
+ "AccountName",
+ "Desc"
+ )
+ ),
+ expectedRemoteEntry
+ )
+ val actualRemoteEntry = beginCreateCredentialResponse.remoteEntry
+
+ assertThat(actualRemoteEntry).isEqualTo(expectedRemoteEntry)
+ }
+
+ @Test
+ fun getter_remoteEntry_nonNull() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val expectedRemoteEntry: RemoteEntry = constructRemoteEntryDefault()
+
+ val beginCreateCredentialResponse = BeginCreateCredentialResponse(
+ listOf(
+ constructCreateEntryWithSimpleParams(
+ "AccountName",
+ "Desc"
+ )
+ ),
+ expectedRemoteEntry
+ )
+ val actualRemoteEntry = beginCreateCredentialResponse.remoteEntry
+
+ assertThat(actualRemoteEntry).isEqualTo(expectedRemoteEntry)
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreateCustomCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreateCustomCredentialRequestJavaTest.java
new file mode 100644
index 0000000..d299d48
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreateCustomCredentialRequestJavaTest.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.credentials.provider;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.pm.SigningInfo;
+import android.os.Bundle;
+import android.service.credentials.CallingAppInfo;
+
+import androidx.core.os.BuildCompat;
+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;
+
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BeginCreateCustomCredentialRequestJavaTest {
+
+ @Test
+ public void constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ new BeginCreateCustomCredentialRequest("type", Bundle.EMPTY, null);
+ }
+
+ @Test
+ public void constructor_nullTypeBundle_throws() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ // TODO(b/275416815) - parameterize to account for all individually
+ assertThrows("Expected null list to throw NPE",
+ NullPointerException.class,
+ () -> new BeginCreateCustomCredentialRequest(null, null, new CallingAppInfo(
+ "package", new SigningInfo()))
+ );
+ }
+
+ @Test
+ public void getter_type() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ String expectedType = "ironman";
+
+ BeginCreateCustomCredentialRequest beginCreateCustomCredentialRequest =
+ new BeginCreateCustomCredentialRequest(expectedType, Bundle.EMPTY, null);
+ String actualType = beginCreateCustomCredentialRequest.getType();
+
+ assertThat(actualType).isEqualTo(expectedType);
+ }
+
+ @Test
+ public void getter_bundle() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ String expectedKey = "query";
+ String expectedValue = "data";
+ Bundle expectedBundle = new Bundle();
+ expectedBundle.putString(expectedKey, expectedValue);
+
+ BeginCreateCustomCredentialRequest beginCreateCustomCredentialRequest =
+ new BeginCreateCustomCredentialRequest("type", expectedBundle, null);
+ Bundle actualBundle = beginCreateCustomCredentialRequest.getCandidateQueryData();
+
+ assertThat(actualBundle.getString(expectedKey)).isEqualTo(expectedValue);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreateCustomCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreateCustomCredentialRequestTest.kt
new file mode 100644
index 0000000..67d5605d
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreateCustomCredentialRequestTest.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.credentials.provider
+
+import android.os.Bundle
+import androidx.core.os.BuildCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class BeginCreateCustomCredentialRequestTest {
+
+ @Test
+ fun constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ BeginCreateCustomCredentialRequest("type", Bundle.EMPTY, null)
+ }
+
+ @Test
+ fun getter_type() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val expectedType = "ironman"
+ val beginCreateCustomCredentialRequest =
+ BeginCreateCustomCredentialRequest(expectedType, Bundle.EMPTY, null)
+ val actualType = beginCreateCustomCredentialRequest.type
+ assertThat(actualType).isEqualTo(expectedType)
+ }
+
+ @Test
+ fun getter_bundle() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val expectedKey = "query"
+ val expectedValue = "data"
+ val expectedBundle = Bundle()
+ expectedBundle.putString(expectedKey, expectedValue)
+ val beginCreateCustomCredentialRequest =
+ BeginCreateCustomCredentialRequest("type", expectedBundle, null)
+ val actualBundle = beginCreateCustomCredentialRequest.candidateQueryData
+ assertThat(actualBundle.getString(expectedKey)).isEqualTo(expectedValue)
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreatePasswordRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreatePasswordRequestJavaTest.java
new file mode 100644
index 0000000..e263c18
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreatePasswordRequestJavaTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.credentials.provider;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.pm.SigningInfo;
+import android.os.Bundle;
+import android.service.credentials.CallingAppInfo;
+
+import androidx.core.os.BuildCompat;
+import androidx.credentials.TestUtilsKt;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BeginCreatePasswordRequestJavaTest {
+ @Test
+ public void constructor_success() {
+ if (BuildCompat.isAtLeastU()) {
+ new BeginCreatePasswordCredentialRequest(
+ new CallingAppInfo("sample_package_name",
+ new SigningInfo()),
+ new Bundle());
+ }
+ }
+
+ @Test
+ public void getter_callingAppInfo() {
+ if (BuildCompat.isAtLeastU()) {
+ Bundle expectedCandidateQueryBundle = new Bundle();
+ expectedCandidateQueryBundle.putString("key", "value");
+ String expectedPackageName = "sample_package_name";
+ SigningInfo expectedSigningInfo = new SigningInfo();
+ CallingAppInfo expectedCallingAppInfo = new CallingAppInfo(expectedPackageName,
+ expectedSigningInfo);
+
+ BeginCreatePasswordCredentialRequest request =
+ new BeginCreatePasswordCredentialRequest(expectedCallingAppInfo,
+ expectedCandidateQueryBundle);
+
+ assertThat(request.getCallingAppInfo().getPackageName()).isEqualTo(expectedPackageName);
+ assertThat(request.getCallingAppInfo().getSigningInfo()).isEqualTo(expectedSigningInfo);
+ TestUtilsKt.equals(request.getCandidateQueryData(), expectedCandidateQueryBundle);
+ }
+ }
+
+ // TODO ("Add framework conversion, createFrom tests")
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreatePasswordRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreatePasswordRequestTest.kt
new file mode 100644
index 0000000..391842f
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreatePasswordRequestTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.credentials.provider
+
+import android.content.pm.SigningInfo
+import android.os.Bundle
+import android.service.credentials.CallingAppInfo
+import androidx.annotation.RequiresApi
+import androidx.core.os.BuildCompat
+import androidx.credentials.equals
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@RequiresApi(34)
+class BeginCreatePasswordRequestTest {
+ @Test
+ fun constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ BeginCreatePasswordCredentialRequest(
+ CallingAppInfo(
+ "sample_package_name",
+ SigningInfo()
+ ),
+ Bundle()
+ )
+ }
+
+ @Test
+ fun getter_callingAppInfo() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+
+ val expectedCandidateQueryBundle = Bundle()
+ expectedCandidateQueryBundle.putString("key", "value")
+ val expectedPackageName = "sample_package_name"
+ val expectedSigningInfo = SigningInfo()
+ val expectedCallingAppInfo = CallingAppInfo(
+ expectedPackageName,
+ expectedSigningInfo
+ )
+
+ val request = BeginCreatePasswordCredentialRequest(
+ expectedCallingAppInfo, expectedCandidateQueryBundle)
+
+ equals(request.candidateQueryData, expectedCandidateQueryBundle)
+ assertThat(request.callingAppInfo?.packageName).isEqualTo(expectedPackageName)
+ assertThat(request.callingAppInfo?.signingInfo).isEqualTo(expectedSigningInfo)
+ }
+
+ // TODO ("Add framework conversion, createFrom tests")
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreatePublicKeyCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreatePublicKeyCredentialRequestJavaTest.java
new file mode 100644
index 0000000..22308d7
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreatePublicKeyCredentialRequestJavaTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.provider;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.pm.SigningInfo;
+import android.os.Bundle;
+import android.service.credentials.CallingAppInfo;
+
+import androidx.core.os.BuildCompat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BeginCreatePublicKeyCredentialRequestJavaTest {
+ @Test
+ public void constructor_emptyJson_throwsIllegalArgumentException() {
+ if (BuildCompat.isAtLeastU()) {
+ assertThrows("Expected empty Json to throw error",
+ IllegalArgumentException.class,
+ () -> new BeginCreatePublicKeyCredentialRequest(
+ "",
+ new CallingAppInfo(
+ "sample_package_name", new SigningInfo()),
+ new Bundle()
+ )
+ );
+ }
+ }
+
+ @Test
+ public void constructor_nullJson_throwsNullPointerException() {
+ if (BuildCompat.isAtLeastU()) {
+ assertThrows("Expected null Json to throw NPE",
+ NullPointerException.class,
+ () -> new BeginCreatePublicKeyCredentialRequest(
+ null,
+ new CallingAppInfo("sample_package_name",
+ new SigningInfo()),
+ new Bundle()
+ )
+ );
+ }
+ }
+
+ @Test
+ public void constructor_success() {
+ if (BuildCompat.isAtLeastU()) {
+ new BeginCreatePublicKeyCredentialRequest(
+ "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
+ new CallingAppInfo(
+ "sample_package_name", new SigningInfo()
+ ),
+ new Bundle()
+ );
+ }
+ }
+
+ @Test
+ public void constructorWithClientDataHash_success() {
+ if (BuildCompat.isAtLeastU()) {
+ new BeginCreatePublicKeyCredentialRequest(
+ "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
+ new CallingAppInfo(
+ "sample_package_name", new SigningInfo()
+ ),
+ new Bundle(),
+ "client_data_hash".getBytes()
+ );
+ }
+ }
+
+ @Test
+ public void getter_requestJson_success() {
+ if (BuildCompat.isAtLeastU()) {
+ String testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
+
+ BeginCreatePublicKeyCredentialRequest
+ createPublicKeyCredentialReq = new BeginCreatePublicKeyCredentialRequest(
+ testJsonExpected,
+ new CallingAppInfo(
+ "sample_package_name", new SigningInfo()),
+ new Bundle()
+ );
+
+ String testJsonActual = createPublicKeyCredentialReq.getRequestJson();
+ assertThat(testJsonActual).isEqualTo(testJsonExpected);
+ assertThat(createPublicKeyCredentialReq.getClientDataHash()).isNull();
+
+ }
+ }
+
+ @Test
+ public void getter_clientDataHash_success() {
+ if (BuildCompat.isAtLeastU()) {
+ String testClientDataHashExpected = "client_data_hash";
+ BeginCreatePublicKeyCredentialRequest createPublicKeyCredentialReq =
+ new BeginCreatePublicKeyCredentialRequest(
+ "json",
+ new CallingAppInfo("sample_package_name",
+ new SigningInfo()),
+ new Bundle(),
+ testClientDataHashExpected.getBytes());
+
+ assertThat(createPublicKeyCredentialReq.getClientDataHash())
+ .isEqualTo(testClientDataHashExpected.getBytes());
+ }
+ }
+ // TODO ("Add framework conversion, createFrom & preferImmediatelyAvailable tests")
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreatePublicKeyCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreatePublicKeyCredentialRequestTest.kt
new file mode 100644
index 0000000..48b0416
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginCreatePublicKeyCredentialRequestTest.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.credentials.provider
+
+import android.content.pm.SigningInfo
+import android.os.Bundle
+import android.service.credentials.CallingAppInfo
+import androidx.annotation.RequiresApi
+import androidx.core.os.BuildCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+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
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@RequiresApi(34)
+class BeginCreatePublicKeyCredentialRequestTest {
+ @Test
+ fun constructor_emptyJson_throwsIllegalArgumentException() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ Assert.assertThrows(
+ "Expected empty Json to throw error",
+ IllegalArgumentException::class.java
+ ) {
+ BeginCreatePublicKeyCredentialRequest(
+ "",
+ CallingAppInfo(
+ "sample_package_name",
+ SigningInfo()
+ ),
+ Bundle()
+ )
+ }
+ }
+
+ @Test
+ fun constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ BeginCreatePublicKeyCredentialRequest(
+ "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
+ CallingAppInfo(
+ "sample_package_name", SigningInfo()
+ ),
+ Bundle()
+ )
+ }
+
+ @Test
+ fun constructorWithClientDataHash_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ BeginCreatePublicKeyCredentialRequest(
+ "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
+ CallingAppInfo(
+ "sample_package_name", SigningInfo()
+ ),
+ Bundle(),
+ "client_data_hash".toByteArray()
+ )
+ }
+
+ @Test
+ fun getter_requestJson_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
+
+ val createPublicKeyCredentialReq = BeginCreatePublicKeyCredentialRequest(
+ testJsonExpected,
+ CallingAppInfo(
+ "sample_package_name", SigningInfo()
+ ),
+ Bundle()
+ )
+
+ val testJsonActual = createPublicKeyCredentialReq.requestJson
+ assertThat(testJsonActual).isEqualTo(testJsonExpected)
+ assertThat(createPublicKeyCredentialReq.clientDataHash).isNull()
+ }
+
+ @Test
+ fun getter_clientDataHash_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val testClientDataHashExpected = "client_data_hash".toByteArray()
+ val createPublicKeyCredentialReq = BeginCreatePublicKeyCredentialRequest(
+ "json",
+ CallingAppInfo(
+ "sample_package_name", SigningInfo()
+ ),
+ Bundle(),
+ testClientDataHashExpected
+ )
+
+ val testClientDataHashActual = createPublicKeyCredentialReq.clientDataHash
+ assertThat(testClientDataHashActual).isEqualTo(testClientDataHashExpected)
+ }
+ // TODO ("Add framework conversion, createFrom & preferImmediatelyAvailable tests")
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCredentialRequestJavaTest.java
new file mode 100644
index 0000000..e593222
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCredentialRequestJavaTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.credentials.provider;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.pm.SigningInfo;
+import android.os.Bundle;
+import android.service.credentials.CallingAppInfo;
+
+import androidx.core.os.BuildCompat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.List;
+
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BeginGetCredentialRequestJavaTest {
+
+ @Test
+ public void constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ new BeginGetCredentialRequest(Collections.emptyList(), null);
+ }
+
+ @Test
+ public void constructor_nullList_throws() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ assertThrows("Expected null list to throw NPE",
+ NullPointerException.class,
+ () -> new BeginGetCredentialRequest(null,
+ new CallingAppInfo("tom.cruise.security",
+ new SigningInfo()))
+ );
+ }
+
+ @Test
+ public void getter_beginGetCredentialOptions() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ String expectedKey = "query";
+ String expectedValue = "data";
+ Bundle expectedBundle = new Bundle();
+ expectedBundle.putString(expectedKey, expectedValue);
+ String expectedId = "key";
+ String expectedType = "mach-10";
+ int expectedBeginGetCredentialOptionsSize = 1;
+
+ BeginGetCredentialRequest beginGetCredentialRequest =
+ new BeginGetCredentialRequest(Collections.singletonList(
+ new BeginGetCustomCredentialOption(expectedId, expectedType,
+ expectedBundle)),
+ null);
+ List<BeginGetCredentialOption> actualBeginGetCredentialOptionList =
+ beginGetCredentialRequest.getBeginGetCredentialOptions();
+ int actualBeginGetCredentialOptionsSize = actualBeginGetCredentialOptionList.size();
+ assertThat(actualBeginGetCredentialOptionsSize)
+ .isEqualTo(expectedBeginGetCredentialOptionsSize);
+ String actualBundleValue =
+ actualBeginGetCredentialOptionList.get(0).getCandidateQueryData()
+ .getString(expectedKey);
+ String actualId = actualBeginGetCredentialOptionList.get(0).getId();
+ String actualType = actualBeginGetCredentialOptionList.get(0).getType();
+
+ assertThat(actualBundleValue).isEqualTo(expectedValue);
+ assertThat(actualId).isEqualTo(expectedId);
+ assertThat(actualType).isEqualTo(expectedType);
+ }
+
+ @Test
+ public void getter_nullCallingAppInfo() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ CallingAppInfo expectedCallingAppInfo = null;
+
+ BeginGetCredentialRequest beginGetCredentialRequest =
+ new BeginGetCredentialRequest(Collections.emptyList(),
+ expectedCallingAppInfo);
+ CallingAppInfo actualCallingAppInfo = beginGetCredentialRequest.getCallingAppInfo();
+
+ assertThat(actualCallingAppInfo).isEqualTo(expectedCallingAppInfo);
+ }
+
+ @Test
+ public void getter_nonNullCallingAppInfo() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ String expectedPackageName = "john.wick.four.credentials";
+ CallingAppInfo expectedCallingAppInfo = new CallingAppInfo(expectedPackageName,
+ new SigningInfo());
+
+ BeginGetCredentialRequest beginGetCredentialRequest =
+ new BeginGetCredentialRequest(Collections.emptyList(),
+ expectedCallingAppInfo);
+ CallingAppInfo actualCallingAppInfo = beginGetCredentialRequest.getCallingAppInfo();
+ String actualPackageName = actualCallingAppInfo.getPackageName();
+
+ assertThat(actualPackageName).isEqualTo(expectedPackageName);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCredentialRequestTest.kt
new file mode 100644
index 0000000..fbe6d828
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCredentialRequestTest.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.credentials.provider
+
+import android.content.pm.SigningInfo
+import android.os.Bundle
+import android.service.credentials.CallingAppInfo
+import androidx.core.os.BuildCompat
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class BeginGetCredentialRequestTest {
+ @Test
+ fun constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ BeginGetCredentialRequest(emptyList(), null)
+ }
+
+ @Test
+ fun getter_beginGetCredentialOptions() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val expectedKey = "query"
+ val expectedValue = "data"
+ val expectedBundle = Bundle()
+ expectedBundle.putString(expectedKey, expectedValue)
+ val expectedId = "key"
+ val expectedType = "mach-10"
+ val expectedBeginGetCredentialOptionsSize = 1
+
+ val beginGetCredentialRequest = BeginGetCredentialRequest(
+ listOf(
+ BeginGetCustomCredentialOption(expectedId, expectedType, expectedBundle)
+ ),
+ null
+ )
+ val actualBeginGetCredentialOptionList = beginGetCredentialRequest.beginGetCredentialOptions
+ val actualBeginGetCredentialOptionsSize = actualBeginGetCredentialOptionList.size
+ assertThat(actualBeginGetCredentialOptionsSize)
+ .isEqualTo(expectedBeginGetCredentialOptionsSize)
+ val actualBundleValue = actualBeginGetCredentialOptionList[0].candidateQueryData
+ .getString(expectedKey)
+ val actualId = actualBeginGetCredentialOptionList[0].id
+ val actualType = actualBeginGetCredentialOptionList[0].type
+
+ assertThat(actualBundleValue).isEqualTo(expectedValue)
+ assertThat(actualId).isEqualTo(expectedId)
+ assertThat(actualType).isEqualTo(expectedType)
+ }
+
+ @Test
+ fun getter_nullCallingAppInfo() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val expectedCallingAppInfo: CallingAppInfo? = null
+
+ val beginGetCredentialRequest = BeginGetCredentialRequest(
+ emptyList(),
+ expectedCallingAppInfo
+ )
+ val actualCallingAppInfo = beginGetCredentialRequest.callingAppInfo
+
+ assertThat(actualCallingAppInfo).isEqualTo(expectedCallingAppInfo)
+ }
+
+ @Test
+ fun getter_nonNullCallingAppInfo() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val expectedPackageName = "john.wick.four.credentials"
+ val expectedCallingAppInfo = CallingAppInfo(
+ expectedPackageName,
+ SigningInfo()
+ )
+
+ val beginGetCredentialRequest = BeginGetCredentialRequest(
+ emptyList(),
+ expectedCallingAppInfo
+ )
+ val actualCallingAppInfo = beginGetCredentialRequest.callingAppInfo
+ val actualPackageName = actualCallingAppInfo!!.packageName
+
+ assertThat(actualPackageName).isEqualTo(expectedPackageName)
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCredentialResponseJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCredentialResponseJavaTest.java
new file mode 100644
index 0000000..8be08e4
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCredentialResponseJavaTest.java
@@ -0,0 +1,184 @@
+/*
+ * 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.credentials.provider;
+
+import static androidx.credentials.provider.ui.UiUtils.constructActionEntry;
+import static androidx.credentials.provider.ui.UiUtils.constructAuthenticationActionEntry;
+import static androidx.credentials.provider.ui.UiUtils.constructPasswordCredentialEntryDefault;
+import static androidx.credentials.provider.ui.UiUtils.constructRemoteEntryDefault;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.core.os.BuildCompat;
+import androidx.credentials.PasswordCredential;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BeginGetCredentialResponseJavaTest {
+
+ @Test
+ public void constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ new BeginGetCredentialResponse();
+ }
+
+ // TODO(b/275416815) - parameterize to account for all individually
+ @Test
+ public void constructor_nullList_throws() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ assertThrows("Expected null list to throw NPE",
+ NullPointerException.class,
+ () -> new BeginGetCredentialResponse(
+ null, null, null, constructRemoteEntryDefault())
+ );
+ }
+
+ @Test
+ public void buildConstruct_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ new BeginGetCredentialResponse.Builder().build();
+ }
+
+ @Test
+ public void buildConstruct_nullList_throws() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ assertThrows("Expected null list to throw NPE",
+ NullPointerException.class,
+ () -> new BeginGetCredentialResponse.Builder().setCredentialEntries(null)
+ .setActions(null).setAuthenticationActions(null).build()
+ );
+ }
+
+ @Test
+ public void getter_credentialEntries() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ int expectedSize = 1;
+ String expectedType = PasswordCredential.TYPE_PASSWORD_CREDENTIAL;
+ String expectedUsername = "f35";
+
+ BeginGetCredentialResponse response = new BeginGetCredentialResponse(
+ Collections.singletonList(constructPasswordCredentialEntryDefault(
+ expectedUsername)), Collections.emptyList(), Collections.emptyList(),
+ null);
+ int actualSize = response.getCredentialEntries().size();
+ String actualType = response.getCredentialEntries().get(0).getType();
+ String actualUsername = ((PasswordCredentialEntry) response.getCredentialEntries().get(0))
+ .getUsername().toString();
+
+ assertThat(actualSize).isEqualTo(expectedSize);
+ assertThat(actualType).isEqualTo(expectedType);
+ assertThat(actualUsername).isEqualTo(expectedUsername);
+ }
+
+ @Test
+ public void getter_actionEntries() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ int expectedSize = 1;
+ String expectedTitle = "boeing";
+ String expectedSubtitle = "737max";
+
+ BeginGetCredentialResponse response = new BeginGetCredentialResponse(
+ Collections.emptyList(),
+ Collections.singletonList(constructActionEntry(expectedTitle, expectedSubtitle)),
+ Collections.emptyList(), null);
+ int actualSize = response.getActions().size();
+ String actualTitle = response.getActions().get(0).getTitle().toString();
+ String actualSubtitle = response.getActions().get(0).getSubtitle().toString();
+
+ assertThat(actualSize).isEqualTo(expectedSize);
+ assertThat(actualTitle).isEqualTo(expectedTitle);
+ assertThat(actualSubtitle).isEqualTo(expectedSubtitle);
+ }
+
+ @Test
+ public void getter_authActionEntries() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ int expectedSize = 1;
+ String expectedTitle = "boeing";
+
+ BeginGetCredentialResponse response = new BeginGetCredentialResponse(
+ Collections.emptyList(),
+ Collections.emptyList(),
+ Collections.singletonList(constructAuthenticationActionEntry(expectedTitle)), null);
+ int actualSize = response.getAuthenticationActions().size();
+ String actualTitle = response.getAuthenticationActions().get(0).getTitle().toString();
+
+ assertThat(actualSize).isEqualTo(expectedSize);
+ assertThat(actualTitle).isEqualTo(expectedTitle);
+ }
+
+ @Test
+ public void getter_remoteEntry_null() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ RemoteEntry expectedRemoteEntry = null;
+
+ BeginGetCredentialResponse response = new BeginGetCredentialResponse(
+ Collections.emptyList(), Collections.emptyList(), Collections.emptyList(),
+ expectedRemoteEntry
+ );
+ RemoteEntry actualRemoteEntry = response.getRemoteEntry();
+
+ assertThat(actualRemoteEntry).isEqualTo(expectedRemoteEntry);
+ }
+
+ @Test
+ public void getter_remoteEntry_nonNull() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ RemoteEntry expectedRemoteEntry = constructRemoteEntryDefault();
+
+ BeginGetCredentialResponse response = new BeginGetCredentialResponse(
+ Collections.emptyList(), Collections.emptyList(), Collections.emptyList(),
+ expectedRemoteEntry
+ );
+ RemoteEntry actualRemoteEntry = response.getRemoteEntry();
+
+ assertThat(actualRemoteEntry).isEqualTo(expectedRemoteEntry);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCredentialResponseTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCredentialResponseTest.kt
new file mode 100644
index 0000000..c985c92
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCredentialResponseTest.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.credentials.provider
+
+import androidx.core.os.BuildCompat
+import androidx.credentials.PasswordCredential
+import androidx.credentials.provider.ui.UiUtils.Companion.constructActionEntry
+import androidx.credentials.provider.ui.UiUtils.Companion.constructAuthenticationActionEntry
+import androidx.credentials.provider.ui.UiUtils.Companion.constructPasswordCredentialEntryDefault
+import androidx.credentials.provider.ui.UiUtils.Companion.constructRemoteEntryDefault
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class BeginGetCredentialResponseTest {
+
+ @Test
+ fun constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+
+ BeginGetCredentialResponse()
+ }
+
+ @Test
+ fun buildConstruct_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+
+ BeginGetCredentialResponse.Builder().build()
+ }
+
+ @Test
+ fun getter_credentialEntries() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val expectedSize = 1
+ val expectedType = PasswordCredential.TYPE_PASSWORD_CREDENTIAL
+ val expectedUsername = "f35"
+
+ val response = BeginGetCredentialResponse(
+ listOf(
+ constructPasswordCredentialEntryDefault(
+ expectedUsername
+ )
+ )
+ )
+ val actualSize = response.credentialEntries.size
+ val actualType = response.credentialEntries[0].type
+ val actualUsername = (response.credentialEntries[0] as PasswordCredentialEntry)
+ .username.toString()
+
+ assertThat(actualSize).isEqualTo(expectedSize)
+ assertThat(actualType).isEqualTo(expectedType)
+ assertThat(actualUsername).isEqualTo(expectedUsername)
+ }
+
+ @Test
+ fun getter_actionEntries() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val expectedSize = 1
+ val expectedTitle = "boeing"
+ val expectedSubtitle = "737max"
+
+ val response = BeginGetCredentialResponse(
+ emptyList(),
+ listOf(constructActionEntry(expectedTitle, expectedSubtitle))
+ )
+ val actualSize = response.actions.size
+ val actualTitle = response.actions[0].title.toString()
+ val actualSubtitle = response.actions[0].subtitle.toString()
+
+ assertThat(actualSize).isEqualTo(expectedSize)
+ assertThat(actualTitle).isEqualTo(expectedTitle)
+ assertThat(actualSubtitle).isEqualTo(expectedSubtitle)
+ }
+
+ @Test
+ fun getter_authActionEntries() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val expectedSize = 1
+ val expectedTitle = "boeing"
+
+ val response = BeginGetCredentialResponse(
+ emptyList(), emptyList(), listOf(
+ constructAuthenticationActionEntry(expectedTitle)
+ )
+ )
+ val actualSize = response.authenticationActions.size
+ val actualTitle = response.authenticationActions[0].title.toString()
+
+ assertThat(actualSize).isEqualTo(expectedSize)
+ assertThat(actualTitle).isEqualTo(expectedTitle)
+ }
+
+ @Test
+ fun getter_remoteEntry_null() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+
+ val expectedRemoteEntry: RemoteEntry? = null
+ val response = BeginGetCredentialResponse(
+ emptyList(), emptyList(), emptyList(),
+ expectedRemoteEntry
+ )
+ val actualRemoteEntry = response.remoteEntry
+
+ assertThat(actualRemoteEntry).isEqualTo(expectedRemoteEntry)
+ }
+
+ @Test
+ fun getter_remoteEntry_nonNull() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+
+ val expectedRemoteEntry = constructRemoteEntryDefault()
+ val response = BeginGetCredentialResponse(
+ emptyList(), emptyList(), emptyList(),
+ expectedRemoteEntry
+ )
+ val actualRemoteEntry = response.remoteEntry
+
+ assertThat(actualRemoteEntry).isEqualTo(expectedRemoteEntry)
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCustomCredentialOptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCustomCredentialOptionJavaTest.java
new file mode 100644
index 0000000..1394cac
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCustomCredentialOptionJavaTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.provider;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Bundle;
+
+import androidx.core.os.BuildCompat;
+import androidx.credentials.TestUtilsKt;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BeginGetCustomCredentialOptionJavaTest {
+ @Test
+ public void constructor_success() {
+ if (BuildCompat.isAtLeastU()) {
+ Bundle expectedBundle = new Bundle();
+ expectedBundle.putString("random", "random_value");
+ String expectedType = "type";
+ String expectedId = "id";
+
+ BeginGetCustomCredentialOption option = new BeginGetCustomCredentialOption(
+ expectedId, expectedType, expectedBundle);
+
+ assertThat(option.getType()).isEqualTo(expectedType);
+ assertThat(option.getId()).isEqualTo(expectedId);
+ assertThat(TestUtilsKt.equals(option.getCandidateQueryData(), expectedBundle)).isTrue();
+ }
+ }
+
+ @Test
+ public void constructor_emptyType_throwsIAE() {
+ if (BuildCompat.isAtLeastU()) {
+ assertThrows("Expected empty Json to throw error",
+ IllegalArgumentException.class,
+ () -> new BeginGetCustomCredentialOption(
+ "id",
+ "",
+ new Bundle()
+ )
+ );
+ }
+ }
+
+ @Test
+ public void constructor_emptyId_throwsIAE() {
+ if (BuildCompat.isAtLeastU()) {
+ assertThrows("Expected empty Json to throw error",
+ IllegalArgumentException.class,
+ () -> new BeginGetCustomCredentialOption(
+ "",
+ "type",
+ new Bundle()
+ )
+ );
+ }
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCustomCredentialOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCustomCredentialOptionTest.kt
new file mode 100644
index 0000000..f659b8c
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetCustomCredentialOptionTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.credentials.provider
+
+import android.os.Bundle
+import androidx.core.os.BuildCompat
+import androidx.credentials.equals
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class BeginGetCustomCredentialOptionTest {
+ @Test
+ fun constructor_success() {
+ if (BuildCompat.isAtLeastU()) {
+ val expectedBundle = Bundle()
+ expectedBundle.putString("random", "random_value")
+ val expectedType = "type"
+ val expectedId = "id"
+ val option = BeginGetCustomCredentialOption(
+ expectedId, expectedType, expectedBundle
+ )
+ Truth.assertThat(option.type).isEqualTo(expectedType)
+ Truth.assertThat(option.id).isEqualTo(expectedId)
+ Truth.assertThat(equals(option.candidateQueryData, expectedBundle)).isTrue()
+ }
+ }
+
+ @Test
+ fun constructor_emptyType_throwsIAE() {
+ if (BuildCompat.isAtLeastU()) {
+ assertThrows(
+ "Expected empty Json to throw error",
+ IllegalArgumentException::class.java
+ ) {
+ BeginGetCustomCredentialOption(
+ "id",
+ "",
+ Bundle()
+ )
+ }
+ }
+ }
+
+ @Test
+ fun constructor_emptyId_throwsIAE() {
+ if (BuildCompat.isAtLeastU()) {
+ assertThrows(
+ "Expected empty Json to throw error",
+ IllegalArgumentException::class.java
+ ) {
+ BeginGetCustomCredentialOption(
+ "",
+ "type",
+ Bundle()
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetPasswordOptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetPasswordOptionJavaTest.java
new file mode 100644
index 0000000..f73d7a0
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetPasswordOptionJavaTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.credentials.provider;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+
+import androidx.core.os.BuildCompat;
+import androidx.credentials.GetPasswordOption;
+import androidx.credentials.PasswordCredential;
+import androidx.credentials.TestUtilsKt;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BeginGetPasswordOptionJavaTest {
+ private static final String BUNDLE_ID_KEY =
+ "android.service.credentials.BeginGetCredentialOption.BUNDLE_ID_KEY";
+ private static final String BUNDLE_ID = "id";
+ @Test
+ public void getter_frameworkProperties() {
+ if (BuildCompat.isAtLeastU()) {
+ Set<String> expectedAllowedUserIds = ImmutableSet.of("id1", "id2", "id3");
+ Bundle bundle = new Bundle();
+ bundle.putStringArrayList(GetPasswordOption.BUNDLE_KEY_ALLOWED_USER_IDS,
+ new ArrayList<>(expectedAllowedUserIds));
+
+ BeginGetPasswordOption option = new BeginGetPasswordOption(expectedAllowedUserIds,
+ bundle, BUNDLE_ID);
+
+ bundle.putString(BUNDLE_ID_KEY, BUNDLE_ID);
+ assertThat(option.getType()).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL);
+ assertThat(TestUtilsKt.equals(option.getCandidateQueryData(), bundle)).isTrue();
+ assertThat(option.getAllowedUserIds())
+ .containsExactlyElementsIn(expectedAllowedUserIds);
+ }
+ }
+
+ // TODO ("Add framework conversion, createFrom tests")
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetPasswordOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetPasswordOptionTest.kt
new file mode 100644
index 0000000..1fd619e
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetPasswordOptionTest.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.credentials.provider
+
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.core.os.BuildCompat
+import androidx.credentials.GetPasswordOption
+import androidx.credentials.PasswordCredential
+import androidx.credentials.equals
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@RequiresApi(34)
+class BeginGetPasswordOptionTest {
+ companion object {
+ private const val BUNDLE_ID_KEY =
+ "android.service.credentials.BeginGetCredentialOption.BUNDLE_ID_KEY"
+ private const val BUNDLE_ID = "id"
+ }
+ @Test
+ fun getter_frameworkProperties() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val expectedAllowedUserIds: Set<String> = setOf("id1", "id2", "id3")
+ val bundle = Bundle()
+ bundle.putStringArrayList(
+ GetPasswordOption.BUNDLE_KEY_ALLOWED_USER_IDS,
+ ArrayList(expectedAllowedUserIds)
+ )
+
+ val option = BeginGetPasswordOption(expectedAllowedUserIds, bundle, BUNDLE_ID)
+
+ bundle.putString(BUNDLE_ID_KEY, BUNDLE_ID)
+ assertThat(option.type).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL)
+ assertThat(equals(option.candidateQueryData, bundle)).isTrue()
+ assertThat(option.allowedUserIds).containsExactlyElementsIn(expectedAllowedUserIds)
+ }
+
+ // TODO ("Add framework conversion, createFrom tests")
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOptionJavaTest.java
new file mode 100644
index 0000000..e0ff56e
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOptionJavaTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.credentials.provider;
+
+import static androidx.credentials.GetPublicKeyCredentialOption.BUNDLE_KEY_REQUEST_JSON;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Bundle;
+
+import androidx.core.os.BuildCompat;
+import androidx.credentials.GetPublicKeyCredentialOption;
+import androidx.credentials.PublicKeyCredential;
+import androidx.credentials.TestUtilsKt;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BeginGetPublicKeyCredentialOptionJavaTest {
+ private static final String BUNDLE_ID_KEY =
+ "android.service.credentials.BeginGetCredentialOption.BUNDLE_ID_KEY";
+ private static final String BUNDLE_ID = "id";
+ @Test
+ public void constructor_emptyJson_throwsIllegalArgumentException() {
+ if (BuildCompat.isAtLeastU()) {
+ assertThrows("Expected empty Json to throw error",
+ IllegalArgumentException.class,
+ () -> new BeginGetPublicKeyCredentialOption(
+ new Bundle(), "", "")
+ );
+ }
+ }
+
+ @Test
+ public void constructor_nullJson_throwsNullPointerException() {
+ if (BuildCompat.isAtLeastU()) {
+ assertThrows("Expected null Json to throw NPE",
+ NullPointerException.class,
+ () -> new BeginGetPublicKeyCredentialOption(
+ new Bundle(), BUNDLE_ID, null)
+ );
+ }
+ }
+
+ @Test
+ public void constructor_success() {
+ if (BuildCompat.isAtLeastU()) {
+ new BeginGetPublicKeyCredentialOption(
+ new Bundle(), BUNDLE_ID,
+ "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}");
+ }
+ }
+
+ @Test
+ public void constructorWithClientDataHash_success() {
+ if (BuildCompat.isAtLeastU()) {
+ new BeginGetPublicKeyCredentialOption(
+ new Bundle(), BUNDLE_ID,
+ "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
+ "client_data_hash".getBytes());
+ }
+ }
+
+ @Test
+ public void getter_requestJson_success() {
+ if (BuildCompat.isAtLeastU()) {
+ String testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
+
+ BeginGetPublicKeyCredentialOption getPublicKeyCredentialOpt =
+ new BeginGetPublicKeyCredentialOption(
+ new Bundle(), BUNDLE_ID, testJsonExpected);
+
+ String testJsonActual = getPublicKeyCredentialOpt.getRequestJson();
+ assertThat(testJsonActual).isEqualTo(testJsonExpected);
+ }
+ }
+
+ @Test
+ public void getter_clientDataHash_success() {
+ if (BuildCompat.isAtLeastU()) {
+ byte[] testClientDataHashExpected = "client_data_hash".getBytes();
+
+ BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOpt =
+ new BeginGetPublicKeyCredentialOption(
+ new Bundle(), BUNDLE_ID, "test_json",
+ testClientDataHashExpected);
+
+ byte[] testClientDataHashActual = beginGetPublicKeyCredentialOpt.getClientDataHash();
+ assertThat(testClientDataHashActual).isEqualTo(testClientDataHashExpected);
+ }
+ }
+
+ @Test
+ public void getter_frameworkProperties_success() {
+ if (BuildCompat.isAtLeastU()) {
+ String requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
+ byte[] clientDataHash = "client_data_hash".getBytes();
+ Bundle expectedData = new Bundle();
+ expectedData.putString(
+ PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
+ GetPublicKeyCredentialOption
+ .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION);
+ expectedData.putString(BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
+ expectedData.putByteArray(GetPublicKeyCredentialOption.BUNDLE_KEY_CLIENT_DATA_HASH,
+ clientDataHash);
+
+ BeginGetPublicKeyCredentialOption option = new BeginGetPublicKeyCredentialOption(
+ expectedData, BUNDLE_ID, requestJsonExpected, clientDataHash);
+
+ expectedData.putString(BUNDLE_ID_KEY, BUNDLE_ID);
+ assertThat(option.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
+ assertThat(TestUtilsKt.equals(option.getCandidateQueryData(), expectedData)).isTrue();
+ }
+ }
+ // TODO ("Add framework conversion, createFrom tests")
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOptionTest.kt
new file mode 100644
index 0000000..66cb74d
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOptionTest.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.credentials.provider
+
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.core.os.BuildCompat
+import androidx.credentials.GetPublicKeyCredentialOption
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.equals
+import androidx.test.ext.junit.runners.AndroidJUnit4
+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
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@RequiresApi(34)
+class BeginGetPublicKeyCredentialOptionTest {
+ companion object {
+ private const val BUNDLE_ID_KEY =
+ "android.service.credentials.BeginGetCredentialOption.BUNDLE_ID_KEY"
+ private const val BUNDLE_ID = "id"
+ }
+ @Test
+ fun constructor_emptyJson_throwsIllegalArgumentException() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ Assert.assertThrows(
+ "Expected empty Json to throw error",
+ IllegalArgumentException::class.java
+ ) {
+ BeginGetPublicKeyCredentialOption(Bundle(), "", "")
+ }
+ }
+
+ @Test
+ fun constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ BeginGetPublicKeyCredentialOption(
+ Bundle(), BUNDLE_ID, "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
+ "client_data_hash".toByteArray()
+ )
+ }
+
+ @Test
+ fun getter_clientDataHash_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val testClientDataHashExpected = "client_data_hash".toByteArray()
+
+ val beginGetPublicKeyCredentialOpt = BeginGetPublicKeyCredentialOption(
+ Bundle(), BUNDLE_ID, "test_json", testClientDataHashExpected
+ )
+
+ val testClientDataHashActual = beginGetPublicKeyCredentialOpt.clientDataHash
+ assertThat(testClientDataHashActual).isEqualTo(testClientDataHashExpected)
+ }
+
+ @Test
+ fun getter_requestJson_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
+
+ val getPublicKeyCredentialOpt = BeginGetPublicKeyCredentialOption(
+ Bundle(), BUNDLE_ID, testJsonExpected
+ )
+
+ val testJsonActual = getPublicKeyCredentialOpt.requestJson
+ assertThat(testJsonActual).isEqualTo(testJsonExpected)
+ }
+
+ @Test
+ fun getter_frameworkProperties_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
+ val clientDataHash = "client_data_hash".toByteArray()
+ val expectedData = Bundle()
+ expectedData.putString(
+ PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
+ GetPublicKeyCredentialOption.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION)
+ expectedData.putString(
+ GetPublicKeyCredentialOption.BUNDLE_KEY_REQUEST_JSON,
+ requestJsonExpected)
+ expectedData.putByteArray(
+ GetPublicKeyCredentialOption.BUNDLE_KEY_CLIENT_DATA_HASH,
+ clientDataHash)
+
+ val option = BeginGetPublicKeyCredentialOption(expectedData, BUNDLE_ID, requestJsonExpected)
+
+ expectedData.putString(BUNDLE_ID_KEY, BUNDLE_ID)
+ assertThat(option.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
+ assertThat(equals(option.candidateQueryData, expectedData)).isTrue()
+ }
+
+ // TODO ("Add framework conversion, createFrom tests")
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderClearCredentialStateRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderClearCredentialStateRequestJavaTest.java
new file mode 100644
index 0000000..ec0620c
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderClearCredentialStateRequestJavaTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.credentials.provider;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.pm.SigningInfo;
+import android.service.credentials.CallingAppInfo;
+
+import androidx.credentials.TestUtilsKt;
+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;
+
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ProviderClearCredentialStateRequestJavaTest {
+
+ @Test
+ public void testConstructor_success() {
+ CallingAppInfo callingAppInfo = new CallingAppInfo(
+ "package", new SigningInfo());
+
+ ProviderClearCredentialStateRequest request = new
+ ProviderClearCredentialStateRequest(callingAppInfo);
+
+ assertThat(TestUtilsKt.equals(request.getCallingAppInfo(), callingAppInfo))
+ .isTrue();
+ }
+
+ @Test
+ public void testConstructor_nullCallingAppInfo_throwsNPE() {
+ assertThrows(
+ NullPointerException.class,
+ () -> new ProviderClearCredentialStateRequest(null)
+ );
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderClearCredentialStateRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderClearCredentialStateRequestTest.kt
new file mode 100644
index 0000000..d2ed5da
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderClearCredentialStateRequestTest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.credentials.provider
+
+import android.content.pm.SigningInfo
+import android.service.credentials.CallingAppInfo
+import androidx.annotation.RequiresApi
+import androidx.credentials.equals
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class ProviderClearCredentialStateRequestTest {
+
+ @RequiresApi(34)
+ @Test
+ fun testConstructor_success() {
+ val callingAppInfo = CallingAppInfo("sample_package_name", SigningInfo())
+
+ val request = ProviderClearCredentialStateRequest(callingAppInfo)
+
+ assertThat(equals(callingAppInfo, request.callingAppInfo)).isTrue()
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderGetCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderGetCredentialRequestJavaTest.java
new file mode 100644
index 0000000..79d8421
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderGetCredentialRequestJavaTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.credentials.provider;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.ComponentName;
+import android.content.pm.SigningInfo;
+import android.os.Bundle;
+import android.service.credentials.CallingAppInfo;
+
+import androidx.core.os.BuildCompat;
+import androidx.credentials.CredentialOption;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ProviderGetCredentialRequestJavaTest {
+
+ @Test
+ public void constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ new ProviderGetCredentialRequest(
+ Collections.singletonList(CredentialOption.createFrom("type", new Bundle(),
+ new Bundle(), true, ImmutableSet.of())),
+ new CallingAppInfo("name",
+ new SigningInfo()));
+ }
+
+ @Test
+ public void constructor_nullInputs_throws() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+
+ assertThrows("Expected null list to throw NPE",
+ NullPointerException.class,
+ () -> new ProviderGetCredentialRequest(null, null)
+ );
+ }
+
+ @Test
+ public void getter_credentialOptions() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ String expectedType = "BoeingCred";
+ String expectedQueryKey = "PilotName";
+ String expectedQueryValue = "PilotPassword";
+ Bundle expectedCandidateQueryData = new Bundle();
+ expectedCandidateQueryData.putString(expectedQueryKey, expectedQueryValue);
+ String expectedRequestKey = "PlaneKey";
+ String expectedRequestValue = "PlaneInfo";
+ Bundle expectedRequestData = new Bundle();
+ expectedRequestData.putString(expectedRequestKey, expectedRequestValue);
+ boolean expectedRequireSystemProvider = true;
+ Set<ComponentName> expectedAllowedProviders = ImmutableSet.of(
+ new ComponentName("pkg", "cls"),
+ new ComponentName("pkg2", "cls2")
+ );
+
+ ProviderGetCredentialRequest providerGetCredentialRequest =
+ new ProviderGetCredentialRequest(
+ Collections.singletonList(CredentialOption.createFrom(expectedType,
+ expectedRequestData,
+ expectedCandidateQueryData,
+ expectedRequireSystemProvider,
+ expectedAllowedProviders)),
+ new CallingAppInfo("name",
+ new SigningInfo()));
+ List<CredentialOption> actualCredentialOptionsList =
+ providerGetCredentialRequest.getCredentialOptions();
+ assertThat(actualCredentialOptionsList.size()).isEqualTo(1);
+ String actualType = actualCredentialOptionsList.get(0).getType();
+ String actualRequestValue =
+ actualCredentialOptionsList.get(0).getRequestData().getString(expectedRequestKey);
+ String actualQueryValue =
+ actualCredentialOptionsList.get(0).getCandidateQueryData()
+ .getString(expectedQueryKey);
+ boolean actualRequireSystemProvider =
+ actualCredentialOptionsList.get(0).isSystemProviderRequired();
+
+ assertThat(actualType).isEqualTo(expectedType);
+ assertThat(actualRequestValue).isEqualTo(expectedRequestValue);
+ assertThat(actualQueryValue).isEqualTo(expectedQueryValue);
+ assertThat(actualRequireSystemProvider).isEqualTo(expectedRequireSystemProvider);
+ assertThat(actualCredentialOptionsList.get(0).getAllowedProviders())
+ .containsAtLeastElementsIn(expectedAllowedProviders);
+ }
+
+ @Test
+ public void getter_signingInfo() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ String expectedPackageName = "cool.security.package";
+
+ ProviderGetCredentialRequest providerGetCredentialRequest =
+ new ProviderGetCredentialRequest(
+ Collections.singletonList(CredentialOption.createFrom("type", new Bundle(),
+ new Bundle(), true, ImmutableSet.of())),
+ new CallingAppInfo(expectedPackageName,
+ new SigningInfo()));
+ String actualPackageName =
+ providerGetCredentialRequest.getCallingAppInfo().getPackageName();
+
+ assertThat(actualPackageName).isEqualTo(expectedPackageName);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderGetCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderGetCredentialRequestTest.kt
new file mode 100644
index 0000000..8e5f0ca
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderGetCredentialRequestTest.kt
@@ -0,0 +1,133 @@
+/*
+ * 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.credentials.provider
+
+import android.content.ComponentName
+import android.content.pm.SigningInfo
+import android.os.Bundle
+import android.service.credentials.CallingAppInfo
+import androidx.core.os.BuildCompat
+import androidx.credentials.CredentialOption.Companion.createFrom
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class ProviderGetCredentialRequestTest {
+
+ @Test
+ fun constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+
+ ProviderGetCredentialRequest(
+ listOf(
+ createFrom(
+ "type", Bundle(),
+ Bundle(), true,
+ emptySet()
+ )
+ ), CallingAppInfo(
+ "name",
+ SigningInfo()
+ )
+ )
+ }
+
+ // TODO(b/275416815) - Test createFrom()
+
+ @Test
+ fun getter_credentialOptions() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val expectedType = "BoeingCred"
+ val expectedQueryKey = "PilotName"
+ val expectedQueryValue = "PilotPassword"
+ val expectedCandidateQueryData = Bundle()
+ expectedCandidateQueryData.putString(expectedQueryKey, expectedQueryValue)
+ val expectedRequestKey = "PlaneKey"
+ val expectedRequestValue = "PlaneInfo"
+ val expectedRequestData = Bundle()
+ expectedRequestData.putString(expectedRequestKey, expectedRequestValue)
+ val expectedRequireSystemProvider = true
+ val expectedAllowedProviders: Set<ComponentName> = setOf(
+ ComponentName("pkg", "cls"),
+ ComponentName("pkg2", "cls2")
+ )
+
+ val providerGetCredentialRequest = ProviderGetCredentialRequest(
+ listOf(
+ createFrom(
+ expectedType,
+ expectedRequestData,
+ expectedCandidateQueryData,
+ expectedRequireSystemProvider,
+ expectedAllowedProviders
+ )
+ ),
+ CallingAppInfo(
+ "name",
+ SigningInfo()
+ )
+ )
+ val actualCredentialOptionsList = providerGetCredentialRequest.credentialOptions
+ assertThat(actualCredentialOptionsList.size).isEqualTo(1)
+ val actualType = actualCredentialOptionsList[0].type
+ val actualRequestValue =
+ actualCredentialOptionsList[0].requestData.getString(expectedRequestKey)
+ val actualQueryValue = actualCredentialOptionsList[0].candidateQueryData
+ .getString(expectedQueryKey)
+ val actualRequireSystemProvider = actualCredentialOptionsList[0].isSystemProviderRequired
+
+ assertThat(actualType).isEqualTo(expectedType)
+ assertThat(actualRequestValue).isEqualTo(expectedRequestValue)
+ assertThat(actualQueryValue).isEqualTo(expectedQueryValue)
+ assertThat(actualRequireSystemProvider).isEqualTo(expectedRequireSystemProvider)
+ assertThat(actualCredentialOptionsList[0].allowedProviders)
+ .containsAtLeastElementsIn(expectedAllowedProviders)
+ }
+
+ @Test
+ fun getter_signingInfo() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val expectedPackageName = "cool.security.package"
+
+ val providerGetCredentialRequest = ProviderGetCredentialRequest(
+ listOf(
+ createFrom(
+ "type", Bundle(),
+ Bundle(), true, emptySet()
+ )
+ ), CallingAppInfo(
+ expectedPackageName,
+ SigningInfo()
+ )
+ )
+ val actualPackageName = providerGetCredentialRequest.callingAppInfo.packageName
+
+ assertThat(actualPackageName).isEqualTo(expectedPackageName)
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..d0bc879
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/ActionJavaTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.credentials.provider.ui;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+
+import android.app.PendingIntent;
+import android.app.slice.Slice;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.core.os.BuildCompat;
+import androidx.credentials.provider.Action;
+import androidx.test.core.app.ApplicationProvider;
+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;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+public class ActionJavaTest {
+ private static final CharSequence TITLE = "title";
+ private static final CharSequence SUBTITLE = "subtitle";
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final Intent mIntent = new Intent();
+ private final PendingIntent mPendingIntent =
+ PendingIntent.getActivity(mContext, 0, mIntent,
+ PendingIntent.FLAG_IMMUTABLE);
+
+
+ @Test
+ public void constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ Action action = new Action(TITLE, mPendingIntent, SUBTITLE);
+
+ assertNotNull(action);
+ assertThat(TITLE.equals(action.getTitle()));
+ assertThat(SUBTITLE.equals(action.getSubtitle()));
+ assertThat(mPendingIntent == action.getPendingIntent());
+ }
+
+ @Test
+ public void constructor_nullTitle_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null title to throw NPE",
+ NullPointerException.class,
+ () -> new Action(null, mPendingIntent, SUBTITLE));
+ }
+
+ @Test
+ public void constructor_nullPendingIntent_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null title to throw NPE",
+ NullPointerException.class,
+ () -> new Action(TITLE, null, SUBTITLE));
+ }
+
+ @Test
+ public void constructor_emptyTitle_throwsIllegalArgumentException() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected empty title to throw IllegalArgumentException",
+ IllegalArgumentException.class,
+ () -> new Action("", mPendingIntent, SUBTITLE));
+ }
+
+ @Test
+ public void fromSlice_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ Action originalAction = new Action(TITLE, mPendingIntent, SUBTITLE);
+ Slice slice = Action.toSlice(originalAction);
+
+ Action fromSlice = Action.fromSlice(slice);
+
+ assertNotNull(fromSlice);
+ assertThat(fromSlice.getTitle()).isEqualTo(TITLE);
+ assertThat(fromSlice.getSubtitle()).isEqualTo(SUBTITLE);
+ assertThat(fromSlice.getPendingIntent()).isEqualTo(mPendingIntent);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/ActionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/ActionTest.kt
new file mode 100644
index 0000000..c075468
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/ActionTest.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.credentials.provider.ui
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import androidx.core.os.BuildCompat
+import androidx.credentials.provider.Action
+import androidx.credentials.provider.Action.Companion.fromSlice
+import androidx.test.core.app.ApplicationProvider
+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.Assert.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+class ActionTest {
+ private val mContext = ApplicationProvider.getApplicationContext<Context>()
+ private val mIntent = Intent()
+ private val mPendingIntent = PendingIntent.getActivity(mContext, 0, mIntent,
+ PendingIntent.FLAG_IMMUTABLE)
+
+ @Test
+ fun constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val action = Action(TITLE, mPendingIntent, SUBTITLE)
+ val slice = Action.toSlice(action)
+
+ assertNotNull(action)
+ assertNotNull(slice)
+ assertThat(TITLE == action.title)
+ assertThat(SUBTITLE == action.subtitle)
+ assertThat(mPendingIntent === action.pendingIntent)
+ }
+
+ @Test
+ fun constructor_emptyTitle_throwsIllegalArgumentException() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ Assert.assertThrows(
+ "Expected empty title to throw IllegalArgumentException",
+ IllegalArgumentException::class.java
+ ) { Action("", mPendingIntent, SUBTITLE) }
+ }
+
+ @Test
+ fun fromSlice_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val originalAction = Action(TITLE, mPendingIntent, SUBTITLE)
+ val slice = Action.toSlice(originalAction)
+
+ val fromSlice = fromSlice(slice)
+
+ assertNotNull(fromSlice)
+ fromSlice?.let {
+ assertThat(fromSlice.title).isEqualTo(TITLE)
+ assertThat(fromSlice.subtitle).isEqualTo(SUBTITLE)
+ assertThat(fromSlice.pendingIntent).isEqualTo(mPendingIntent)
+ }
+ }
+
+ companion object {
+ private val TITLE: CharSequence = "title"
+ private val SUBTITLE: CharSequence = "subtitle"
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..1d2f090
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/AuthenticationActionJavaTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.credentials.provider.ui;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+
+import android.app.PendingIntent;
+import android.app.slice.Slice;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.core.os.BuildCompat;
+import androidx.credentials.provider.AuthenticationAction;
+import androidx.test.core.app.ApplicationProvider;
+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;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+public class AuthenticationActionJavaTest {
+ private static final CharSequence TITLE = "title";
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final Intent mIntent = new Intent();
+ private final PendingIntent mPendingIntent =
+ PendingIntent.getActivity(mContext, 0, mIntent, PendingIntent.FLAG_IMMUTABLE);
+
+ @Test
+ public void constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ AuthenticationAction action = new AuthenticationAction(TITLE, mPendingIntent);
+
+ assertThat(mPendingIntent == action.getPendingIntent());
+ }
+
+ @Test
+ public void constructor_nullPendingIntent_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null pending intent to throw NPE",
+ NullPointerException.class,
+ () -> new AuthenticationAction(TITLE, null));
+ }
+
+ @Test
+ public void fromSlice_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ AuthenticationAction originalAction = new AuthenticationAction(TITLE, mPendingIntent);
+ Slice slice = AuthenticationAction.toSlice(originalAction);
+
+ AuthenticationAction fromSlice = AuthenticationAction.fromSlice(slice);
+
+ assertNotNull(fromSlice);
+ assertThat(fromSlice.getPendingIntent()).isEqualTo(mPendingIntent);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/AuthenticationActionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/AuthenticationActionTest.kt
new file mode 100644
index 0000000..74fbecd
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/AuthenticationActionTest.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.credentials.provider.ui
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import androidx.core.os.BuildCompat
+import androidx.credentials.provider.AuthenticationAction
+import androidx.credentials.provider.AuthenticationAction.Companion.fromSlice
+import androidx.test.core.app.ApplicationProvider
+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.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+class AuthenticationActionTest {
+ private val mContext = ApplicationProvider.getApplicationContext<Context>()
+ private val mIntent = Intent()
+ private val mPendingIntent = PendingIntent.getActivity(mContext, 0, mIntent,
+ PendingIntent.FLAG_IMMUTABLE)
+ @Test
+ fun constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val action = AuthenticationAction(TITLE, mPendingIntent)
+
+ assertThat(mPendingIntent).isEqualTo(action.pendingIntent)
+ }
+
+ @Test
+ fun fromSlice_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val originalAction = AuthenticationAction(TITLE, mPendingIntent)
+ val slice = AuthenticationAction.toSlice(originalAction)
+
+ val fromSlice = fromSlice(slice)
+
+ assertNotNull(fromSlice)
+ fromSlice?.let {
+ assertNotNull(fromSlice.pendingIntent)
+ assertThat(fromSlice.pendingIntent).isEqualTo(mPendingIntent)
+ }
+ }
+
+ companion object {
+ private val TITLE: CharSequence = "title"
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..f2992da
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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.credentials.provider.ui;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Icon;
+
+import androidx.core.os.BuildCompat;
+import androidx.credentials.provider.CreateEntry;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+public class CreateEntryJavaTest {
+ private static final CharSequence ACCOUNT_NAME = "account_name";
+ private static final int PASSWORD_COUNT = 10;
+ private static final int PUBLIC_KEY_CREDENTIAL_COUNT = 10;
+ private static final int TOTAL_COUNT = 10;
+
+ private static final Long LAST_USED_TIME = 10L;
+ private static final Icon ICON = Icon.createWithBitmap(Bitmap.createBitmap(
+ 100, 100, Bitmap.Config.ARGB_8888));
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final Intent mIntent = new Intent();
+ private final PendingIntent mPendingIntent =
+ PendingIntent.getActivity(mContext, 0, mIntent,
+ PendingIntent.FLAG_IMMUTABLE);
+
+ @Test
+ public void constructor_requiredParameters_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ CreateEntry entry = constructEntryWithRequiredParams();
+
+ assertNotNull(entry);
+ assertEntryWithRequiredParams(entry);
+ assertNull(entry.getIcon());
+ assertNull(entry.getLastUsedTime());
+ assertNull(entry.getPasswordCredentialCount());
+ assertNull(entry.getPublicKeyCredentialCount());
+ assertNull(entry.getTotalCredentialCount());
+ }
+
+ @Test
+ public void constructor_allParameters_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ CreateEntry entry = constructEntryWithAllParams();
+
+ assertNotNull(entry);
+ assertEntryWithAllParams(entry);
+ }
+
+ @Test
+ public void constructor_nullAccountName_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null title to throw NPE",
+ NullPointerException.class,
+ () -> new CreateEntry.Builder(
+ null, mPendingIntent).build());
+ }
+
+ @Test
+ public void constructor_nullPendingIntent_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null pending intent to throw NPE",
+ NullPointerException.class,
+ () -> new CreateEntry.Builder(ACCOUNT_NAME, null).build());
+ }
+
+ @Test
+ public void constructor_emptyAccountName_throwsIAE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected empty account name to throw NPE",
+ IllegalArgumentException.class,
+ () -> new CreateEntry.Builder("", mPendingIntent).build());
+ }
+
+ @Test
+ public void fromSlice_requiredParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ CreateEntry originalEntry = constructEntryWithRequiredParams();
+
+ CreateEntry entry = CreateEntry.fromSlice(
+ CreateEntry.toSlice(originalEntry));
+
+ assertNotNull(entry);
+ assertEntryWithRequiredParams(entry);
+ }
+
+ @Test
+ public void fromSlice_allParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ CreateEntry originalEntry = constructEntryWithAllParams();
+
+ CreateEntry entry = CreateEntry.fromSlice(
+ CreateEntry.toSlice(originalEntry));
+
+ assertNotNull(entry);
+ assertEntryWithAllParams(entry);
+ }
+
+ private CreateEntry constructEntryWithRequiredParams() {
+ return new CreateEntry.Builder(ACCOUNT_NAME, mPendingIntent).build();
+ }
+
+ private void assertEntryWithRequiredParams(CreateEntry entry) {
+ assertThat(ACCOUNT_NAME.equals(entry.getAccountName()));
+ assertThat(mPendingIntent).isEqualTo(entry.getPendingIntent());
+ }
+
+ private CreateEntry constructEntryWithAllParams() {
+ return 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();
+ }
+
+ private void assertEntryWithAllParams(CreateEntry entry) {
+ assertThat(ACCOUNT_NAME).isEqualTo(entry.getAccountName());
+ assertThat(mPendingIntent).isEqualTo(entry.getPendingIntent());
+ assertThat(ICON).isEqualTo(entry.getIcon());
+ assertThat(Instant.ofEpochMilli(LAST_USED_TIME)).isEqualTo(entry.getLastUsedTime());
+ assertThat(PASSWORD_COUNT).isEqualTo(entry.getPasswordCredentialCount());
+ assertThat(PUBLIC_KEY_CREDENTIAL_COUNT).isEqualTo(entry.getPublicKeyCredentialCount());
+ assertThat(TOTAL_COUNT).isEqualTo(entry.getTotalCredentialCount());
+ }
+}
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
new file mode 100644
index 0000000..4affab6
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt
@@ -0,0 +1,181 @@
+/*
+ * 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.credentials.provider.ui
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import androidx.core.os.BuildCompat
+import androidx.credentials.provider.CreateEntry
+import androidx.credentials.provider.CreateEntry.Companion.fromSlice
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import java.time.Instant
+import org.junit.Assert
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreateEntryTest {
+ private val mContext = ApplicationProvider.getApplicationContext<Context>()
+ private val mIntent = Intent()
+ private val mPendingIntent = PendingIntent.getActivity(
+ mContext, 0, mIntent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+
+ @Test
+ fun constructor_requiredParameters_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val entry = constructEntryWithRequiredParams()
+
+ assertNotNull(entry)
+ assertEntryWithRequiredParams(entry)
+ assertNull(entry.icon)
+ assertNull(entry.lastUsedTime)
+ assertNull(entry.getPasswordCredentialCount())
+ assertNull(entry.getPublicKeyCredentialCount())
+ assertNull(entry.getTotalCredentialCount())
+ }
+
+ @Test
+ fun constructor_allParameters_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val entry = constructEntryWithAllParams()
+
+ assertNotNull(entry)
+ assertEntryWithAllParams(entry)
+ }
+
+ @Test
+ fun constructor_emptyAccountName_throwsIAE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ Assert.assertThrows(
+ "Expected empty account name to throw NPE",
+ IllegalArgumentException::class.java
+ ) {
+ CreateEntry(
+ "", mPendingIntent
+ )
+ }
+ }
+
+ @Test
+ fun fromSlice_requiredParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val originalEntry = constructEntryWithRequiredParams()
+
+ val entry = fromSlice(CreateEntry.toSlice(originalEntry))
+
+ assertNotNull(entry)
+ entry?.let {
+ assertEntryWithRequiredParams(entry)
+ }
+ }
+
+ @Test
+ fun fromSlice_allParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val originalEntry = constructEntryWithAllParams()
+
+ val entry = fromSlice(CreateEntry.toSlice(originalEntry))
+
+ assertNotNull(entry)
+ entry?.let {
+ assertEntryWithAllParams(entry)
+ }
+ }
+
+ private fun constructEntryWithRequiredParams(): CreateEntry {
+ return CreateEntry(
+ ACCOUNT_NAME,
+ mPendingIntent
+ )
+ }
+
+ private fun assertEntryWithRequiredParams(entry: CreateEntry) {
+ Truth.assertThat(ACCOUNT_NAME == entry.accountName)
+ Truth.assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
+ }
+
+ private fun constructEntryWithAllParams(): CreateEntry {
+ return CreateEntry(
+ ACCOUNT_NAME,
+ mPendingIntent,
+ DESCRIPTION,
+ Instant.ofEpochMilli(LAST_USED_TIME),
+ ICON,
+ PASSWORD_COUNT,
+ PUBLIC_KEY_CREDENTIAL_COUNT,
+ TOTAL_COUNT
+ )
+ }
+
+ 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()
+ )
+ }
+
+ companion object {
+ private val ACCOUNT_NAME: CharSequence = "account_name"
+ private const val DESCRIPTION = "description"
+ private const val PASSWORD_COUNT = 10
+ private const val PUBLIC_KEY_CREDENTIAL_COUNT = 10
+ private const val TOTAL_COUNT = 10
+ private const val LAST_USED_TIME = 10L
+ private val ICON = Icon.createWithBitmap(
+ Bitmap.createBitmap(
+ 100, 100, Bitmap.Config.ARGB_8888
+ )
+ )
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..41bba92
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java
@@ -0,0 +1,264 @@
+/*
+ * 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.credentials.provider.ui;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+
+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.CustomCredentialEntry;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+public class CustomCredentialEntryJavaTest {
+ private static final CharSequence TITLE = "title";
+ private static final CharSequence SUBTITLE = "subtitle";
+
+ private static final String TYPE = "custom_type";
+ private static final CharSequence TYPE_DISPLAY_NAME = "Password";
+ private static final Long LAST_USED_TIME = 10L;
+ private static final Icon ICON = Icon.createWithBitmap(Bitmap.createBitmap(
+ 100, 100, Bitmap.Config.ARGB_8888));
+ private static final boolean IS_AUTO_SELECT_ALLOWED = true;
+ private final BeginGetCredentialOption mBeginCredentialOption =
+ new BeginGetCustomCredentialOption(
+ "id", "custom", new Bundle());
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final Intent mIntent = new Intent();
+ private final PendingIntent mPendingIntent =
+ PendingIntent.getActivity(mContext, 0, mIntent,
+ PendingIntent.FLAG_IMMUTABLE);
+
+ @Test
+ public void build_requiredParameters_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ CustomCredentialEntry entry = constructEntryWithRequiredParams();
+
+ assertNotNull(entry);
+ assertNotNull(entry.getSlice());
+ assertEntryWithRequiredParams(entry);
+ }
+
+ @Test
+ public void build_allParameters_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ CustomCredentialEntry entry = constructEntryWithAllParams();
+
+ assertNotNull(entry);
+ assertNotNull(entry.getSlice());
+ assertEntryWithAllParams(entry);
+ }
+
+ @Test
+ public void build_nullTitle_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null title to throw NPE",
+ NullPointerException.class,
+ () -> new CustomCredentialEntry.Builder(
+ mContext, TYPE, null, mPendingIntent, mBeginCredentialOption
+ ));
+ }
+
+ @Test
+ public void build_nullContext_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null title to throw NPE",
+ NullPointerException.class,
+ () -> new CustomCredentialEntry.Builder(
+ null, TYPE, TITLE, mPendingIntent, mBeginCredentialOption
+ ).build());
+ }
+
+ @Test
+ public void build_nullPendingIntent_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null pending intent to throw NPE",
+ NullPointerException.class,
+ () -> new CustomCredentialEntry.Builder(
+ mContext, TYPE, TITLE, null, mBeginCredentialOption
+ ).build());
+ }
+
+ @Test
+ public void build_nullBeginOption_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null option to throw NPE",
+ NullPointerException.class,
+ () -> new CustomCredentialEntry.Builder(
+ mContext, TYPE, TITLE, mPendingIntent, null
+ ).build());
+ }
+
+ @Test
+ public void build_emptyTitle_throwsIAE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected empty title to throw IAE",
+ IllegalArgumentException.class,
+ () -> new CustomCredentialEntry.Builder(
+ mContext, TYPE, "", mPendingIntent, mBeginCredentialOption
+ ).build());
+ }
+
+ @Test
+ public void build_emptyType_throwsIAE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected empty type to throw NPE",
+ IllegalArgumentException.class,
+ () -> new CustomCredentialEntry.Builder(
+ mContext, "", TITLE, mPendingIntent, mBeginCredentialOption
+ ).build());
+ }
+
+ @Test
+ public void build_nullIcon_defaultIconSet() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ CustomCredentialEntry entry = constructEntryWithRequiredParams();
+
+ assertThat(TestUtilsKt.equals(entry.getIcon(),
+ Icon.createWithResource(mContext, R.drawable.ic_other_sign_in))).isTrue();
+ }
+
+ @Test
+ public void fromSlice_requiredParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ CustomCredentialEntry originalEntry = constructEntryWithRequiredParams();
+
+ CustomCredentialEntry entry = CustomCredentialEntry.fromSlice(
+ originalEntry.getSlice());
+
+ assertNotNull(entry);
+ assertEntryWithRequiredParamsFromSlice(entry);
+ }
+
+ @Test
+ public void fromSlice_allParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ CustomCredentialEntry originalEntry = constructEntryWithAllParams();
+
+ CustomCredentialEntry entry = CustomCredentialEntry.fromSlice(
+ originalEntry.getSlice());
+
+ assertNotNull(entry);
+ assertEntryWithAllParamsFromSlice(entry);
+ }
+
+ private CustomCredentialEntry constructEntryWithRequiredParams() {
+ return new CustomCredentialEntry.Builder(
+ mContext,
+ TYPE,
+ TITLE,
+ mPendingIntent,
+ mBeginCredentialOption
+ ).build();
+ }
+
+ private CustomCredentialEntry constructEntryWithAllParams() {
+ return new CustomCredentialEntry.Builder(
+ mContext,
+ TYPE,
+ TITLE,
+ mPendingIntent,
+ mBeginCredentialOption)
+ .setIcon(ICON)
+ .setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME))
+ .setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED)
+ .setTypeDisplayName(TYPE_DISPLAY_NAME)
+ .build();
+ }
+
+ private void assertEntryWithRequiredParams(CustomCredentialEntry entry) {
+ assertThat(TITLE.equals(entry.getTitle()));
+ assertThat(TYPE.equals(entry.getType()));
+ assertThat(mPendingIntent).isEqualTo(entry.getPendingIntent());
+ }
+
+ private void assertEntryWithRequiredParamsFromSlice(CustomCredentialEntry entry) {
+ assertThat(TITLE.equals(entry.getTitle()));
+ assertThat(TYPE.equals(entry.getType()));
+ assertThat(mPendingIntent).isEqualTo(entry.getPendingIntent());
+ }
+
+ private void assertEntryWithAllParams(CustomCredentialEntry entry) {
+ assertThat(TITLE.equals(entry.getTitle()));
+ assertThat(TYPE.equals(entry.getType()));
+ assertThat(SUBTITLE.equals(entry.getSubtitle()));
+ assertThat(TYPE_DISPLAY_NAME.equals(entry.getTypeDisplayName()));
+ assertThat(ICON).isEqualTo(entry.getIcon());
+ assertThat(Instant.ofEpochMilli(LAST_USED_TIME)).isEqualTo(entry.getLastUsedTime());
+ assertThat(IS_AUTO_SELECT_ALLOWED).isEqualTo(entry.isAutoSelectAllowed());
+ assertThat(mPendingIntent).isEqualTo(entry.getPendingIntent());
+ // TODO: Assert BeginOption
+ }
+
+ private void assertEntryWithAllParamsFromSlice(CustomCredentialEntry entry) {
+ assertThat(TITLE.equals(entry.getTitle()));
+ assertThat(TYPE.equals(entry.getType()));
+ assertThat(SUBTITLE.equals(entry.getSubtitle()));
+ assertThat(TYPE_DISPLAY_NAME.equals(entry.getTypeDisplayName()));
+ assertThat(ICON).isEqualTo(entry.getIcon());
+ assertThat(Instant.ofEpochMilli(LAST_USED_TIME)).isEqualTo(entry.getLastUsedTime());
+ assertThat(IS_AUTO_SELECT_ALLOWED).isEqualTo(entry.isAutoSelectAllowed());
+ assertThat(mPendingIntent).isEqualTo(entry.getPendingIntent());
+ // TODO: Assert BeginOption
+ }
+}
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
new file mode 100644
index 0000000..061e241
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt
@@ -0,0 +1,237 @@
+/*
+ * 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.credentials.provider.ui
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.os.Bundle
+import androidx.credentials.provider.BeginGetCredentialOption
+import androidx.core.os.BuildCompat
+import androidx.credentials.R
+import androidx.credentials.equals
+import androidx.credentials.provider.BeginGetCustomCredentialOption
+import androidx.credentials.provider.CustomCredentialEntry
+import androidx.credentials.provider.CustomCredentialEntry.Companion.fromSlice
+import androidx.test.core.app.ApplicationProvider
+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 java.time.Instant
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CustomCredentialEntryTest {
+ private val mContext = ApplicationProvider.getApplicationContext<Context>()
+ private val mIntent = Intent()
+ private val mPendingIntent = PendingIntent.getActivity(mContext, 0, mIntent,
+ PendingIntent.FLAG_IMMUTABLE)
+ @Test
+ fun constructor_requiredParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val entry = constructEntryWithRequiredParams()
+
+ assertNotNull(entry)
+ assertNotNull(entry.slice)
+ assertEntryWithRequiredParams(entry)
+ }
+
+ @Test
+ fun constructor_allParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val entry = constructEntryWithAllParams()
+
+ assertNotNull(entry)
+ assertNotNull(entry.slice)
+ assertEntryWithAllParams(entry)
+ }
+
+ @Test
+ fun constructor_allParameters_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val entry: CustomCredentialEntry = constructEntryWithAllParams()
+
+ assertNotNull(entry)
+ assertNotNull(entry.slice)
+ assertEntryWithAllParams(entry)
+ }
+
+ @Test
+ fun constructor_emptyTitle_throwsIAE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ assertThrows(
+ "Expected empty title to throw NPE",
+ IllegalArgumentException::class.java
+ ) {
+ CustomCredentialEntry(
+ mContext, TITLE, mPendingIntent, BeginGetCustomCredentialOption(
+ "id", "", Bundle.EMPTY
+ )
+ )
+ }
+ }
+
+ @Test
+ fun constructor_emptyType_throwsIAE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ assertThrows(
+ "Expected empty type to throw NPE",
+ IllegalArgumentException::class.java
+ ) {
+ CustomCredentialEntry(
+ mContext, TITLE, mPendingIntent, BeginGetCustomCredentialOption(
+ "id", "", Bundle.EMPTY)
+ )
+ }
+ }
+
+ @Test
+ fun constructor_nullIcon_defaultIconSet() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val entry = constructEntryWithRequiredParams()
+
+ assertThat(
+ equals(
+ entry.icon,
+ Icon.createWithResource(mContext, R.drawable.ic_other_sign_in)
+ )
+ ).isTrue()
+ }
+
+ @Test
+ fun fromSlice_requiredParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val originalEntry = constructEntryWithRequiredParams()
+
+ val entry = fromSlice(originalEntry.slice)
+
+ assertNotNull(entry)
+ if (entry != null) {
+ assertEntryWithRequiredParamsFromSlice(entry)
+ }
+ }
+
+ @Test
+ fun fromSlice_allParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val originalEntry = constructEntryWithAllParams()
+
+ val entry = fromSlice(originalEntry.slice)
+
+ assertNotNull(entry)
+ if (entry != null) {
+ assertEntryWithAllParamsFromSlice(entry)
+ }
+ }
+
+ private fun constructEntryWithRequiredParams(): CustomCredentialEntry {
+ return CustomCredentialEntry(
+ mContext,
+ TITLE,
+ mPendingIntent,
+ BEGIN_OPTION
+ )
+ }
+
+ 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
+ )
+ }
+
+ private fun assertEntryWithAllParams(entry: CustomCredentialEntry) {
+ assertThat(TITLE == entry.title)
+ assertThat(TYPE == entry.type)
+ assertThat(SUBTITLE == entry.subtitle)
+ assertThat(TYPE_DISPLAY_NAME == entry.typeDisplayName)
+ assertThat(ICON).isEqualTo(entry.icon)
+ assertThat(Instant.ofEpochMilli(LAST_USED_TIME)).isEqualTo(entry.lastUsedTime)
+ assertThat(IS_AUTO_SELECT_ALLOWED).isEqualTo(entry.isAutoSelectAllowed)
+ assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
+ }
+
+ private fun assertEntryWithAllParamsFromSlice(entry: CustomCredentialEntry) {
+ assertThat(TITLE == entry.title)
+ assertThat(TYPE == entry.type)
+ assertThat(SUBTITLE == entry.subtitle)
+ assertThat(TYPE_DISPLAY_NAME == entry.typeDisplayName)
+ assertThat(ICON).isEqualTo(entry.icon)
+ assertThat(Instant.ofEpochMilli(LAST_USED_TIME)).isEqualTo(entry.lastUsedTime)
+ assertThat(IS_AUTO_SELECT_ALLOWED).isEqualTo(entry.isAutoSelectAllowed)
+ assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
+ // TODO: Assert BeginOption
+ }
+
+ private fun assertEntryWithRequiredParams(entry: CustomCredentialEntry) {
+ assertThat(TITLE == entry.title)
+ assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
+ // TODO: Assert BeginOption
+ }
+
+ private fun assertEntryWithRequiredParamsFromSlice(entry: CustomCredentialEntry) {
+ assertThat(TITLE == entry.title)
+ assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
+ // TODO: Assert BeginOption
+ }
+
+ companion object {
+ private val TITLE: CharSequence = "title"
+ private val BEGIN_OPTION: BeginGetCredentialOption = BeginGetCustomCredentialOption(
+ "id", "type", Bundle())
+ private val SUBTITLE: CharSequence = "subtitle"
+ private const val TYPE = "custom_type"
+ private val TYPE_DISPLAY_NAME: CharSequence = "Password"
+ private const val LAST_USED_TIME: Long = 10L
+ private val ICON = Icon.createWithBitmap(
+ Bitmap.createBitmap(
+ 100, 100, Bitmap.Config.ARGB_8888
+ )
+ )
+ private const val IS_AUTO_SELECT_ALLOWED = true
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..94e2caa
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java
@@ -0,0 +1,261 @@
+/*
+ * 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.credentials.provider.ui;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+
+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.PasswordCredentialEntry;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+import java.util.HashSet;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+public class PasswordCredentialEntryJavaTest {
+ private static final CharSequence USERNAME = "title";
+ private static final CharSequence DISPLAYNAME = "subtitle";
+ private static final CharSequence TYPE_DISPLAY_NAME = "Password";
+ private static final Long LAST_USED_TIME = 10L;
+
+ private static final boolean IS_AUTO_SELECT_ALLOWED = true;
+
+ private static final Icon ICON = Icon.createWithBitmap(Bitmap.createBitmap(
+ 100, 100, Bitmap.Config.ARGB_8888));
+ private final BeginGetPasswordOption mBeginGetPasswordOption = new BeginGetPasswordOption(
+ new HashSet<>(),
+ Bundle.EMPTY, "id");
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final Intent mIntent = new Intent();
+ private final PendingIntent mPendingIntent =
+ PendingIntent.getActivity(mContext, 0, mIntent,
+ PendingIntent.FLAG_IMMUTABLE);
+
+ @Test
+ public void build_requiredParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ PasswordCredentialEntry entry = constructEntryWithRequiredParamsOnly();
+
+ assertNotNull(entry);
+ assertNotNull(entry.getSlice());
+ assertThat(entry.getType()).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL);
+ assertEntryWithRequiredParamsOnly(entry, false);
+ }
+
+ @Test
+ public void build_allParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ PasswordCredentialEntry entry = constructEntryWithAllParams();
+
+ assertNotNull(entry);
+ assertNotNull(entry.getSlice());
+ assertThat(entry.getType()).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL);
+ assertEntryWithAllParams(entry);
+ }
+
+ @Test
+ public void build_nullContext_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null context to throw NPE",
+ NullPointerException.class,
+ () -> new PasswordCredentialEntry.Builder(
+ null, USERNAME, mPendingIntent, mBeginGetPasswordOption
+ ).build());
+ }
+
+ @Test
+ public void build_nullUsername_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null username to throw NPE",
+ NullPointerException.class,
+ () -> new PasswordCredentialEntry.Builder(
+ mContext, null, mPendingIntent, mBeginGetPasswordOption
+ ).build());
+ }
+
+ @Test
+ public void build_nullPendingIntent_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null pending intent to throw NPE",
+ NullPointerException.class,
+ () -> new PasswordCredentialEntry.Builder(
+ mContext, USERNAME, null, mBeginGetPasswordOption
+ ).build());
+ }
+
+ @Test
+ public void build_nullBeginOption_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null option to throw NPE",
+ NullPointerException.class,
+ () -> new PasswordCredentialEntry.Builder(
+ mContext, USERNAME, mPendingIntent, null
+ ).build());
+ }
+
+ @Test
+ public void build_emptyUsername_throwsIAE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected empty username to throw IllegalArgumentException",
+ IllegalArgumentException.class,
+ () -> new PasswordCredentialEntry.Builder(
+ mContext, "", mPendingIntent, mBeginGetPasswordOption).build());
+ }
+
+ @Test
+ public void build_nullIcon_defaultIconSet() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ PasswordCredentialEntry entry = new PasswordCredentialEntry
+ .Builder(mContext, USERNAME, mPendingIntent, mBeginGetPasswordOption).build();
+
+ assertThat(TestUtilsKt.equals(entry.getIcon(),
+ Icon.createWithResource(mContext, R.drawable.ic_password))).isTrue();
+ }
+
+ @Test
+ public void build_nullTypeDisplayName_defaultDisplayNameSet() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(
+ mContext, USERNAME, mPendingIntent, mBeginGetPasswordOption).build();
+
+ assertThat(entry.getTypeDisplayName()).isEqualTo(
+ mContext.getString(
+ R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL)
+ );
+ }
+
+ @Test
+ public void build_isAutoSelectAllowedDefault_false() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ PasswordCredentialEntry entry = constructEntryWithRequiredParamsOnly();
+
+ assertFalse(entry.isAutoSelectAllowed());
+ }
+
+ @Test
+ public void fromSlice_requiredParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ PasswordCredentialEntry originalEntry = constructEntryWithRequiredParamsOnly();
+
+ assertNotNull(originalEntry.getSlice());
+ PasswordCredentialEntry entry = PasswordCredentialEntry.fromSlice(
+ originalEntry.getSlice());
+
+ assertNotNull(entry);
+ assertEntryWithRequiredParamsOnly(entry, true);
+ }
+
+ @Test
+ public void fromSlice_allParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ PasswordCredentialEntry originalEntry = constructEntryWithAllParams();
+
+ assertNotNull(originalEntry.getSlice());
+ PasswordCredentialEntry entry = PasswordCredentialEntry.fromSlice(
+ originalEntry.getSlice());
+
+ assertNotNull(entry);
+ assertEntryWithAllParams(entry);
+ }
+
+ private PasswordCredentialEntry constructEntryWithRequiredParamsOnly() {
+ return new PasswordCredentialEntry.Builder(
+ mContext,
+ USERNAME,
+ 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)
+ .build();
+ }
+
+ private void assertEntryWithRequiredParamsOnly(PasswordCredentialEntry entry,
+ Boolean assertOptionIdOnly) {
+ assertThat(USERNAME.equals(entry.getUsername()));
+ assertThat(mPendingIntent).isEqualTo(entry.getPendingIntent());
+ // TODO: Assert BeginOption
+ }
+
+ private void assertEntryWithAllParams(PasswordCredentialEntry entry) {
+ assertThat(USERNAME.equals(entry.getUsername()));
+ assertThat(DISPLAYNAME.equals(entry.getDisplayName()));
+ assertThat(TYPE_DISPLAY_NAME.equals(entry.getTypeDisplayName()));
+ assertThat(ICON).isEqualTo(entry.getIcon());
+ assertThat(IS_AUTO_SELECT_ALLOWED).isEqualTo(entry.isAutoSelectAllowed());
+ assertThat(Instant.ofEpochMilli(LAST_USED_TIME)).isEqualTo(entry.getLastUsedTime());
+ assertThat(mPendingIntent).isEqualTo(entry.getPendingIntent());
+ // TODO: Assert BeginOption
+ }
+}
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
new file mode 100644
index 0000000..6c86911
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt
@@ -0,0 +1,206 @@
+/*
+ * 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.credentials.provider.ui
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.os.Bundle
+import androidx.core.os.BuildCompat
+import androidx.credentials.PasswordCredential
+import androidx.credentials.R
+import androidx.credentials.equals
+import androidx.credentials.provider.BeginGetPasswordOption
+import androidx.credentials.provider.PasswordCredentialEntry
+import androidx.credentials.provider.PasswordCredentialEntry.Companion.fromSlice
+import androidx.test.core.app.ApplicationProvider
+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 java.time.Instant
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertNotNull
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class PasswordCredentialEntryTest {
+ private val mContext = ApplicationProvider.getApplicationContext<Context>()
+ private val mIntent = Intent()
+ private val mPendingIntent = PendingIntent.getActivity(
+ mContext, 0, mIntent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+
+ @Test
+ fun constructor_requiredParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val entry = constructEntryWithRequiredParamsOnly()
+
+ assertNotNull(entry)
+ assertNotNull(entry.slice)
+ assertThat(entry.type).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL)
+ assertEntryWithRequiredParamsOnly(entry)
+ }
+
+ @Test
+ fun constructor_allParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val entry = constructEntryWithAllParams()
+
+ assertNotNull(entry)
+ assertNotNull(entry.slice)
+ assertThat(entry.type).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL)
+ assertEntryWithAllParams(entry)
+ }
+
+ @Test
+ fun constructor_emptyUsername_throwsIAE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ assertThrows(
+ "Expected empty username to throw IllegalArgumentException",
+ IllegalArgumentException::class.java
+ ) {
+ PasswordCredentialEntry(
+ mContext, "", mPendingIntent, BEGIN_OPTION
+ )
+ }
+ }
+
+ @Test
+ fun constructor_nullIcon_defaultIconSet() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val entry = PasswordCredentialEntry.Builder(
+ mContext, USERNAME, mPendingIntent, BEGIN_OPTION).build()
+
+ assertThat(
+ equals(
+ entry.icon,
+ Icon.createWithResource(mContext, R.drawable.ic_password)
+ )
+ ).isTrue()
+ }
+
+ @Test
+ fun constructor_nullTypeDisplayName_defaultDisplayNameSet() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val entry = PasswordCredentialEntry(
+ mContext, USERNAME, mPendingIntent, BEGIN_OPTION)
+
+ assertThat(entry.typeDisplayName).isEqualTo(
+ mContext.getString(
+ R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL
+ )
+ )
+ }
+
+ @Test
+ fun constructor_isAutoSelectAllowedDefault_false() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val entry = constructEntryWithRequiredParamsOnly()
+ val entry1 = constructEntryWithAllParams()
+
+ assertFalse(entry.isAutoSelectAllowed)
+ assertFalse(entry1.isAutoSelectAllowed)
+ }
+
+ @Test
+ fun fromSlice_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val originalEntry = constructEntryWithAllParams()
+
+ val entry = fromSlice(originalEntry.slice)
+
+ assertNotNull(entry)
+ entry?.let {
+ assertEntryWithAllParams(entry)
+ }
+ }
+
+ private fun constructEntryWithRequiredParamsOnly(): PasswordCredentialEntry {
+ return PasswordCredentialEntry(
+ mContext,
+ USERNAME,
+ mPendingIntent,
+ BEGIN_OPTION
+ )
+ }
+
+ private fun constructEntryWithAllParams(): PasswordCredentialEntry {
+ return PasswordCredentialEntry(
+ mContext,
+ USERNAME,
+ mPendingIntent,
+ BEGIN_OPTION,
+ DISPLAYNAME,
+ LAST_USED_TIME,
+ ICON
+ )
+ }
+
+ private fun assertEntryWithRequiredParamsOnly(entry: PasswordCredentialEntry) {
+ assertThat(USERNAME == entry.username)
+ assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
+ }
+
+ private fun assertEntryWithAllParams(entry: PasswordCredentialEntry) {
+ assertThat(USERNAME == entry.username)
+ assertThat(DISPLAYNAME == entry.displayName)
+ assertThat(TYPE_DISPLAY_NAME == entry.typeDisplayName)
+ assertThat(ICON).isEqualTo(entry.icon)
+ assertNotNull(entry.lastUsedTime)
+ entry.lastUsedTime?.let {
+ assertThat(LAST_USED_TIME.toEpochMilli()).isEqualTo(
+ it.toEpochMilli())
+ }
+ assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
+ }
+
+ companion object {
+ private val USERNAME: CharSequence = "title"
+ private val DISPLAYNAME: CharSequence = "subtitle"
+ private val TYPE_DISPLAY_NAME: CharSequence = "Password"
+ private val LAST_USED_TIME = Instant.now()
+ private val BEGIN_OPTION = BeginGetPasswordOption(
+ emptySet<String>(),
+ Bundle.EMPTY, "id")
+ private val ICON = Icon.createWithBitmap(
+ Bitmap.createBitmap(
+ 100, 100, Bitmap.Config.ARGB_8888
+ )
+ )
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..aefc55a
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java
@@ -0,0 +1,216 @@
+/*
+ * 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.credentials.provider.ui;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+
+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.PublicKeyCredentialEntry;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+public class PublicKeyCredentialEntryJavaTest {
+ private static final CharSequence USERNAME = "title";
+ private static final CharSequence DISPLAYNAME = "subtitle";
+ private static final CharSequence TYPE_DISPLAY_NAME = "Password";
+ private static final Long LAST_USED_TIME = 10L;
+ private static final Icon ICON = Icon.createWithBitmap(Bitmap.createBitmap(
+ 100, 100, Bitmap.Config.ARGB_8888));
+ private static final boolean IS_AUTO_SELECT_ALLOWED = true;
+ private final BeginGetPublicKeyCredentialOption mBeginOption =
+ new BeginGetPublicKeyCredentialOption(
+ new Bundle(), "id", "json");
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final Intent mIntent = new Intent();
+ private final PendingIntent mPendingIntent =
+ PendingIntent.getActivity(mContext, 0, mIntent,
+ PendingIntent.FLAG_IMMUTABLE);
+
+ @Test
+ public void build_requiredParamsOnly_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ PublicKeyCredentialEntry entry = constructWithRequiredParamsOnly();
+
+ assertNotNull(entry);
+ assertNotNull(entry.getSlice());
+ assertThat(entry.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
+ assertEntryWithRequiredParams(entry);
+ }
+
+ @Test
+ public void build_allParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ PublicKeyCredentialEntry entry = constructWithAllParams();
+
+ assertNotNull(entry);
+ assertNotNull(entry.getSlice());
+ assertThat(entry.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
+ assertEntryWithAllParams(entry);
+ }
+
+ @Test
+ public void build_withNullUsername_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null username to throw NPE",
+ NullPointerException.class,
+ () -> new PublicKeyCredentialEntry.Builder(
+ mContext, null, mPendingIntent, mBeginOption
+ ).build());
+ }
+
+ @Test
+ public void build_withNullBeginOption_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null option to throw NPE",
+ NullPointerException.class,
+ () -> new PublicKeyCredentialEntry.Builder(
+ mContext, USERNAME, mPendingIntent, null
+ ).build());
+ }
+
+ @Test
+ public void build_withNullPendingIntent_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null pending intent to throw NPE",
+ NullPointerException.class,
+ () -> new PublicKeyCredentialEntry.Builder(
+ mContext, USERNAME, null, mBeginOption
+ ).build());
+ }
+
+ @Test
+ public void build_withEmptyUsername_throwsIAE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected empty username to throw IllegalArgumentException",
+ IllegalArgumentException.class,
+ () -> new PublicKeyCredentialEntry.Builder(
+ mContext, "", mPendingIntent, mBeginOption).build());
+ }
+
+ @Test
+ public void build_withNullIcon_defaultIconSet() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ PublicKeyCredentialEntry entry = new PublicKeyCredentialEntry
+ .Builder(
+ mContext, USERNAME, mPendingIntent, mBeginOption).build();
+
+ assertThat(TestUtilsKt.equals(entry.getIcon(),
+ Icon.createWithResource(mContext, R.drawable.ic_passkey))).isTrue();
+ }
+
+ @Test
+ public void build_nullTypeDisplayName_defaultDisplayNameSet() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ PublicKeyCredentialEntry entry = new PublicKeyCredentialEntry.Builder(
+ mContext, USERNAME, mPendingIntent, mBeginOption).build();
+
+ assertThat(entry.getTypeDisplayName()).isEqualTo(
+ mContext.getString(
+ R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL)
+ );
+ }
+
+ @Test
+ public void fromSlice_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ PublicKeyCredentialEntry originalEntry = constructWithAllParams();
+ assertNotNull(originalEntry.getSlice());
+
+ PublicKeyCredentialEntry entry = PublicKeyCredentialEntry.fromSlice(
+ originalEntry.getSlice());
+
+ assertNotNull(entry);
+ assertEntryWithRequiredParams(entry);
+ }
+
+ private PublicKeyCredentialEntry constructWithRequiredParamsOnly() {
+ return new PublicKeyCredentialEntry.Builder(
+ mContext,
+ USERNAME,
+ mPendingIntent,
+ mBeginOption
+ ).build();
+ }
+
+ private PublicKeyCredentialEntry constructWithAllParams() {
+ return new PublicKeyCredentialEntry.Builder(
+ mContext, USERNAME, mPendingIntent, mBeginOption)
+ .setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED)
+ .setDisplayName(DISPLAYNAME)
+ .setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME))
+ .setIcon(ICON)
+ .build();
+ }
+
+ private void assertEntryWithRequiredParams(PublicKeyCredentialEntry entry) {
+ assertThat(USERNAME.equals(entry.getUsername()));
+ assertThat(mPendingIntent).isEqualTo(entry.getPendingIntent());
+ }
+
+ private void assertEntryWithAllParams(PublicKeyCredentialEntry entry) {
+ assertThat(USERNAME.equals(entry.getUsername()));
+ assertThat(DISPLAYNAME.equals(entry.getDisplayName()));
+ assertThat(TYPE_DISPLAY_NAME.equals(entry.getTypeDisplayName()));
+ assertThat(ICON).isEqualTo(entry.getIcon());
+ assertThat(Instant.ofEpochMilli(LAST_USED_TIME)).isEqualTo(entry.getLastUsedTime());
+ assertThat(IS_AUTO_SELECT_ALLOWED).isEqualTo(entry.isAutoSelectAllowed());
+ assertThat(mPendingIntent).isEqualTo(entry.getPendingIntent());
+ }
+}
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
new file mode 100644
index 0000000..93c9eba
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt
@@ -0,0 +1,188 @@
+/*
+ * 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.credentials.provider.ui
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.os.Bundle
+import androidx.core.os.BuildCompat
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.R
+import androidx.credentials.equals
+import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
+import androidx.credentials.provider.PublicKeyCredentialEntry
+import androidx.credentials.provider.PublicKeyCredentialEntry.Companion.fromSlice
+import androidx.test.core.app.ApplicationProvider
+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 java.time.Instant
+import junit.framework.TestCase.assertNotNull
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class PublicKeyCredentialEntryTest {
+ private val mContext = ApplicationProvider.getApplicationContext<Context>()
+ private val mIntent = Intent()
+ private val mPendingIntent = PendingIntent.getActivity(
+ mContext, 0, mIntent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+
+ @Test
+ fun constructor_requiredParamsOnly_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val entry = constructWithRequiredParamsOnly()
+
+ assertNotNull(entry)
+ assertNotNull(entry.slice)
+ assertThat(entry.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
+ assertEntryWithRequiredParams(entry)
+ }
+
+ @Test
+ fun constructor_allParams_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val entry = constructWithAllParams()
+ assertNotNull(entry)
+ assertNotNull(entry.slice)
+ assertThat(entry.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
+ assertEntryWithAllParams(entry)
+ }
+
+ @Test
+ fun constructor_emptyUsername_throwsIAE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ assertThrows(
+ "Expected empty username to throw IllegalArgumentException",
+ IllegalArgumentException::class.java
+ ) {
+ PublicKeyCredentialEntry(
+ mContext, "", mPendingIntent, BEGIN_OPTION
+ )
+ }
+ }
+
+ @Test
+ fun constructor_nullIcon_defaultIconSet() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val entry = PublicKeyCredentialEntry(
+ mContext, USERNAME, mPendingIntent, BEGIN_OPTION
+ )
+
+ assertThat(
+ equals(
+ entry.icon,
+ Icon.createWithResource(mContext, R.drawable.ic_passkey)
+ )
+ ).isTrue()
+ }
+
+ @Test
+ fun constructor_nullTypeDisplayName_defaultDisplayNameSet() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val entry = PublicKeyCredentialEntry(
+ mContext, USERNAME, mPendingIntent, BEGIN_OPTION
+ )
+ assertThat(entry.typeDisplayName).isEqualTo(
+ mContext.getString(
+ R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL
+ )
+ )
+ }
+
+ @Test
+ fun fromSlice_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val originalEntry = constructWithAllParams()
+
+ val entry = fromSlice(originalEntry.slice)
+
+ assertNotNull(entry)
+ entry?.let {
+ assertEntryWithRequiredParams(entry)
+ }
+ }
+
+ private fun constructWithRequiredParamsOnly(): PublicKeyCredentialEntry {
+ return PublicKeyCredentialEntry(
+ mContext,
+ USERNAME,
+ mPendingIntent,
+ BEGIN_OPTION
+ )
+ }
+
+ private fun constructWithAllParams(): PublicKeyCredentialEntry {
+ return PublicKeyCredentialEntry(
+ mContext,
+ USERNAME,
+ mPendingIntent,
+ BEGIN_OPTION,
+ DISPLAYNAME,
+ Instant.ofEpochMilli(LAST_USED_TIME),
+ ICON,
+ IS_AUTO_SELECT_ALLOWED
+ )
+ }
+
+ private fun assertEntryWithRequiredParams(entry: PublicKeyCredentialEntry) {
+ assertThat(USERNAME == entry.username)
+ assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
+ }
+
+ private fun assertEntryWithAllParams(entry: PublicKeyCredentialEntry) {
+ assertThat(USERNAME == entry.username)
+ assertThat(DISPLAYNAME == entry.displayName)
+ assertThat(TYPE_DISPLAY_NAME == entry.typeDisplayName)
+ assertThat(ICON).isEqualTo(entry.icon)
+ assertThat(Instant.ofEpochMilli(LAST_USED_TIME)).isEqualTo(entry.lastUsedTime)
+ assertThat(IS_AUTO_SELECT_ALLOWED).isEqualTo(entry.isAutoSelectAllowed)
+ assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
+ }
+
+ companion object {
+ private val BEGIN_OPTION: BeginGetPublicKeyCredentialOption =
+ BeginGetPublicKeyCredentialOption(Bundle(), "id", "json")
+ private val USERNAME: CharSequence = "title"
+ private val DISPLAYNAME: CharSequence = "subtitle"
+ private val TYPE_DISPLAY_NAME: CharSequence = "Password"
+ private const val LAST_USED_TIME: Long = 10L
+ private val ICON = Icon.createWithBitmap(Bitmap.createBitmap(
+ 100, 100, Bitmap.Config.ARGB_8888))
+ private const val IS_AUTO_SELECT_ALLOWED = true
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..6c45580
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/RemoteEntryJavaTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.credentials.provider.ui;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.core.os.BuildCompat;
+import androidx.credentials.provider.RemoteEntry;
+import androidx.test.core.app.ApplicationProvider;
+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;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+public class RemoteEntryJavaTest {
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final Intent mIntent = new Intent();
+ private final PendingIntent mPendingIntent =
+ PendingIntent.getActivity(mContext, 0, mIntent,
+ PendingIntent.FLAG_IMMUTABLE);
+
+ @Test
+ public void constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ RemoteEntry entry = new RemoteEntry(mPendingIntent);
+
+ assertNotNull(entry);
+ assertThat(mPendingIntent).isEqualTo(entry.getPendingIntent());
+ }
+
+ @Test
+ public void constructor_nullPendingIntent_throwsNPE() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ assertThrows("Expected null pending intent to throw NPE",
+ NullPointerException.class,
+ () -> new RemoteEntry(null));
+ }
+
+ @Test
+ public void fromSlice_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return;
+ }
+ RemoteEntry originalEntry = new RemoteEntry(mPendingIntent);
+
+ RemoteEntry fromSlice = RemoteEntry.fromSlice(RemoteEntry.toSlice(originalEntry));
+
+ assertThat(fromSlice).isNotNull();
+ assertThat(fromSlice.getPendingIntent()).isEqualTo(mPendingIntent);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/RemoteEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/RemoteEntryTest.kt
new file mode 100644
index 0000000..fd54771
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/RemoteEntryTest.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.credentials.provider.ui
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import androidx.core.os.BuildCompat
+import androidx.credentials.provider.RemoteEntry
+import androidx.credentials.provider.RemoteEntry.Companion.fromSlice
+import androidx.test.core.app.ApplicationProvider
+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 junit.framework.TestCase.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+class RemoteEntryTest {
+ private val mContext = ApplicationProvider.getApplicationContext<Context>()
+ private val mIntent = Intent()
+ private val mPendingIntent = PendingIntent.getActivity(mContext, 0, mIntent,
+ PendingIntent.FLAG_IMMUTABLE)
+
+ @Test
+ fun constructor_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val entry = RemoteEntry(mPendingIntent)
+
+ assertNotNull(entry)
+ assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
+ }
+
+ @Test
+ fun fromSlice_success() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val originalEntry = RemoteEntry(mPendingIntent)
+
+ val fromSlice = fromSlice(RemoteEntry.toSlice(originalEntry))
+
+ assertThat(fromSlice).isNotNull()
+ if (fromSlice != null) {
+ assertThat(fromSlice.pendingIntent).isEqualTo(mPendingIntent)
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/UiUtils.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/UiUtils.kt
new file mode 100644
index 0000000..60cd479
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/UiUtils.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.credentials.provider.ui
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.os.Bundle
+import androidx.credentials.provider.Action
+import androidx.credentials.provider.AuthenticationAction
+import androidx.credentials.provider.BeginGetPasswordOption
+import androidx.credentials.provider.CreateEntry
+import androidx.credentials.provider.CredentialEntry
+import androidx.credentials.provider.PasswordCredentialEntry
+import androidx.credentials.provider.RemoteEntry
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SdkSuppress
+
+@SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+class UiUtils {
+ companion object {
+ private val sContext = ApplicationProvider.getApplicationContext<Context>()
+ private val sIntent = Intent()
+ private val sPendingIntent = PendingIntent.getActivity(
+ sContext, 0, sIntent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ private val ACCOUNT_NAME: CharSequence = "account_name"
+ private const val DESCRIPTION = "description"
+ private const val PASSWORD_COUNT = 10
+ private const val PUBLIC_KEY_CREDENTIAL_COUNT = 10
+ private const val TOTAL_COUNT = 10
+ private const val LAST_USED_TIME = 10L
+ private val ICON = Icon.createWithBitmap(
+ Bitmap.createBitmap(
+ 100, 100, Bitmap.Config.ARGB_8888
+ )
+ )
+ private val BEGIN_OPTION = BeginGetPasswordOption(
+ setOf(), Bundle.EMPTY, "id"
+ )
+
+ /**
+ * Generates a default authentication action entry that can be used for tests around the
+ * provider objects.
+ */
+ @JvmStatic
+ fun constructAuthenticationActionEntry(title: CharSequence): AuthenticationAction {
+ return AuthenticationAction(title, sPendingIntent)
+ }
+
+ /**
+ * Generates a default action entry that can be used for tests around the provider
+ * objects.
+ */
+ @JvmStatic
+ fun constructActionEntry(title: CharSequence, subtitle: CharSequence): Action {
+ return Action(title, sPendingIntent, subtitle)
+ }
+
+ /**
+ * Generates a default password credential entry that can be used for tests around the
+ * provider objects.
+ */
+ @JvmStatic
+ fun constructPasswordCredentialEntryDefault(username: CharSequence): CredentialEntry {
+ return PasswordCredentialEntry(
+ sContext,
+ username,
+ sPendingIntent,
+ BEGIN_OPTION
+ )
+ }
+
+ /**
+ * Generate a default remote entry that can be used for tests around the provider objects.
+ */
+ @JvmStatic
+ fun constructRemoteEntryDefault(): RemoteEntry {
+ return RemoteEntry(sPendingIntent)
+ }
+
+ /**
+ * Generates a create entry with known inputs for accountName and description in order
+ * to test proper formation.
+ *
+ * @param accountName the account name associated with the create entry
+ * @param description the description associated with the create entry
+ */
+ @JvmStatic
+ fun constructCreateEntryWithSimpleParams(
+ accountName: CharSequence,
+ description: CharSequence
+ ):
+ CreateEntry {
+ return CreateEntry.Builder(accountName, sPendingIntent).setDescription(description)
+ .build()
+ }
+
+ @JvmStatic
+ fun constructRemoteEntry():
+ RemoteEntry {
+ return RemoteEntry(sPendingIntent)
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
index 10c6a67..937d93e 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
@@ -20,7 +20,6 @@
import android.os.Bundle
import android.text.TextUtils
import androidx.annotation.RequiresApi
-import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.credentials.PublicKeyCredential.Companion.BUNDLE_KEY_SUBTYPE
import androidx.credentials.internal.FrameworkClassParsingException
@@ -31,33 +30,46 @@
* An application can construct a subtype request and call [CredentialManager.createCredential] to
* launch framework UI flows to collect consent and any other metadata needed from the user to
* register a new user credential.
+ *
+ * @property type the credential type determined by the credential-type-specific subclass (e.g.
+ * the type for [CreatePasswordRequest] is [PasswordCredential.TYPE_PASSWORD_CREDENTIAL] and for
+ * [CreatePublicKeyCredentialRequest] is [PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL])
+ * @property credentialData the request data in the [Bundle] format
+ * @property candidateQueryData the partial request data in the [Bundle] format that will be sent
+ * to the provider during the initial candidate query stage, which should not contain sensitive
+ * user credential information (note: bundle keys in the form of `androidx.credentials.*` are
+ * reserved for internal library use)
+ * @property isSystemProviderRequired true if must only be fulfilled by a system provider and
+ * false otherwise
+ * @property isAutoSelectAllowed whether a create option will be automatically chosen if it is
+ * the only one available to the user
+ * @property displayInfo the information to be displayed on the screen
+ * @property origin the origin of a different application if the request is being made on behalf of
+ * that application (Note: for API level >=34, setting a non-null value for this parameter will
+ * throw a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present)
+ * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * immediately when there is no available passkey registration offering instead of falling back to
+ * discovering remote options, and false (preferred by default) otherwise
*/
abstract class CreateCredentialRequest internal constructor(
- /** @hide */
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- open val type: String,
- /** @hide */
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- open val credentialData: Bundle,
- /** @hide */
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- open val candidateQueryData: Bundle,
- /** @hide */
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- open val isSystemProviderRequired: Boolean,
- /** @hide */
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- open val isAutoSelectAllowed: Boolean,
- /** @hide */
+ val type: String,
+ val credentialData: Bundle,
+ val candidateQueryData: Bundle,
+ val isSystemProviderRequired: Boolean,
+ val isAutoSelectAllowed: Boolean,
val displayInfo: DisplayInfo,
val origin: String?,
+ @get:JvmName("preferImmediatelyAvailableCredentials")
+ val preferImmediatelyAvailableCredentials: Boolean,
) {
init {
- @Suppress("UNNECESSARY_SAFE_CALL")
- credentialData?.let {
- credentialData.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, isAutoSelectAllowed)
- }
+ credentialData.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, isAutoSelectAllowed)
+ credentialData.putBoolean(
+ BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+ preferImmediatelyAvailableCredentials
+ )
+ candidateQueryData.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, isAutoSelectAllowed)
}
/**
@@ -75,7 +87,7 @@
/** @hide */
val credentialTypeIcon: Icon?,
/** @hide */
- val defaultProvider: String?,
+ val preferDefaultProvider: String?,
) {
/**
@@ -97,6 +109,31 @@
null,
)
+ /**
+ * Constructs a [DisplayInfo].
+ *
+ * @param userId the user id of the created credential
+ * @param userDisplayName an optional display name in addition to the [userId] that may be
+ * displayed next to the `userId` during the user consent to help your user better
+ * understand the credential being created
+ * @param preferDefaultProvider the preferred default provider component name to prioritize in the
+ * selection UI flows. Your app must have the permission
+ * android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS to specify this, or it
+ * would not take effect. Also this bit may not take effect for Android API level 33 and
+ * below, depending on the pre-34 provider(s) you have chosen.
+ * @throws IllegalArgumentException If [userId] is empty
+ */
+ constructor(
+ userId: CharSequence,
+ userDisplayName: CharSequence?,
+ preferDefaultProvider: String?
+ ) : this(
+ userId,
+ userDisplayName,
+ null,
+ preferDefaultProvider,
+ )
+
init {
require(userId.isNotEmpty()) { "userId should not be empty" }
}
@@ -109,8 +146,8 @@
if (!TextUtils.isEmpty(userDisplayName)) {
bundle.putCharSequence(BUNDLE_KEY_USER_DISPLAY_NAME, userDisplayName)
}
- if (!TextUtils.isEmpty(defaultProvider)) {
- bundle.putString(BUNDLE_KEY_DEFAULT_PROVIDER, defaultProvider)
+ if (!TextUtils.isEmpty(preferDefaultProvider)) {
+ bundle.putString(BUNDLE_KEY_DEFAULT_PROVIDER, preferDefaultProvider)
}
// Today the type icon is determined solely within this library right before the
// request is passed into the framework. Later if needed a new API can be added for
@@ -171,6 +208,8 @@
/** @hide */
companion object {
+ internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
+ "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
/** @hide */
const val BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED =
"androidx.credentials.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED"
@@ -218,7 +257,10 @@
DisplayInfo.parseFromCredentialDataBundle(
credentialData
) ?: return null,
- credentialData.getBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, false)
+ credentialData.getBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, false),
+ origin,
+ credentialData.getBoolean(
+ BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS, false),
)
}
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialResponse.kt b/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialResponse.kt
index 92bc244..3f60032 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialResponse.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialResponse.kt
@@ -17,20 +17,20 @@
package androidx.credentials
import android.os.Bundle
-import androidx.annotation.RestrictTo
import androidx.credentials.internal.FrameworkClassParsingException
/**
* Base response class for the credential creation operation made with the
* [CreateCredentialRequest].
+ *
+ * @property type the credential type determined by the credential-type-specific subclass (e.g.
+ * the type for [CreatePasswordResponse] is [PasswordCredential.TYPE_PASSWORD_CREDENTIAL] and for
+ * [CreatePublicKeyCredentialResponse] is [PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL])
+ * @property data the response data in the [Bundle] format
*/
abstract class CreateCredentialResponse internal constructor(
- /** @hide */
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- open val type: String,
- /** @hide */
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- open val data: Bundle,
+ val type: String,
+ val data: Bundle,
) {
/** @hide */
companion object {
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialRequest.kt
index 09c6b6f..b5f580e 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialRequest.kt
@@ -36,32 +36,36 @@
* @param type the credential type determined by the credential-type-specific subclass for
* custom use cases
* @param credentialData the data of this [CreateCustomCredentialRequest] in the [Bundle]
- * format (note: bundle keys in the form of `androidx.credentials.*` are reserved for internal
- * library use)
+ * format (note: bundle keys in the form of `androidx.credentials.*` and `android.credentials.*` are
+ * reserved for internal library usage)
* @param candidateQueryData the partial request data in the [Bundle] format that will be sent
* to the provider during the initial candidate query stage, which should not contain sensitive
- * user credential information (note: bundle keys in the form of `androidx.credentials.*` are
- * reserved for internal library use)
+ * user credential information (note: bundle keys in the form of `androidx.credentials.*` and
+ * `android.credentials.*` are reserved for internal library usage)
* @param isSystemProviderRequired true if must only be fulfilled by a system provider and
* false otherwise
* @param isAutoSelectAllowed defines if a create entry will be automatically chosen if it is
* the only one available option, false by default
* @param displayInfo the information to be displayed on the screen
* @param origin the origin of a different application if the request is being made on behalf of
- * that application. For API level >=34, setting a non-null value for this parameter, will throw
- * a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present.
+ * that application (Note: for API level >=34, setting a non-null value for this parameter will
+ * throw a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present)
+ * @param preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * immediately when there is no available passkey registration offering instead of falling back to
+ * discovering remote options, and false (default) otherwise
* @throws IllegalArgumentException If [type] is empty
* @throws NullPointerException If [type], [credentialData], or [candidateQueryData] is null
*/
open class CreateCustomCredentialRequest
@JvmOverloads constructor(
- final override val type: String,
- final override val credentialData: Bundle,
- final override val candidateQueryData: Bundle,
- final override val isSystemProviderRequired: Boolean,
+ type: String,
+ credentialData: Bundle,
+ candidateQueryData: Bundle,
+ isSystemProviderRequired: Boolean,
displayInfo: DisplayInfo,
- final override val isAutoSelectAllowed: Boolean = false,
+ isAutoSelectAllowed: Boolean = false,
origin: String? = null,
+ preferImmediatelyAvailableCredentials: Boolean = false,
) : CreateCredentialRequest(
type,
credentialData,
@@ -69,9 +73,9 @@
isSystemProviderRequired,
isAutoSelectAllowed,
displayInfo,
- origin
+ origin,
+ preferImmediatelyAvailableCredentials
) {
-
init {
require(type.isNotEmpty()) { "type should not be empty" }
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialResponse.kt b/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialResponse.kt
index caf0dac..a623a2e 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialResponse.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialResponse.kt
@@ -30,15 +30,17 @@
* Note: The Bundle keys for [data] should not be in the form of androidx.credentials.*` as they
* are reserved for internal use by this androidx library.
*
- * @property type the credential type determined by the credential-type-specific subclass for custom
+ * @param type the credential type determined by the credential-type-specific subclass for custom
* use cases
- * @property data the response data in the [Bundle] format for custom use cases
+ * @param data the response data in the [Bundle] format for custom use cases (note: bundle keys in
+ * the form of `androidx.credentials.*` and `android.credentials.*` are reserved for internal
+ * library usage)
* @throws IllegalArgumentException If [type] is empty
* @throws NullPointerException If [type] or [data] are null
*/
open class CreateCustomCredentialResponse(
- final override val type: String,
- final override val data: Bundle
+ type: String,
+ data: Bundle
) : CreateCredentialResponse(type, data) {
init {
require(type.isNotEmpty()) { "type should not be empty" }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordRequest.kt
index 62b970b..de720a5 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordRequest.kt
@@ -26,16 +26,21 @@
*
* @property id the user id associated with the password
* @property password the password
+ * @param id the user id associated with the password
+ * @param password the password
* @param origin the origin of a different application if the request is being made on behalf of
- * that application. For API level >=34, setting a non-null value for this parameter, will throw a
- * SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present when
- * API level >= 34.
+ * that application (Note: for API level >=34, setting a non-null value for this parameter will
+ * throw a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present)
+ * @param preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * immediately when there is no available credential creation offering instead of falling back to
+ * discovering remote options, and false (default) otherwise
*/
class CreatePasswordRequest private constructor(
val id: String,
val password: String,
displayInfo: DisplayInfo,
origin: String? = null,
+ preferImmediatelyAvailableCredentials: Boolean,
) : CreateCredentialRequest(
type = PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
credentialData = toCredentialDataBundle(id, password),
@@ -43,7 +48,8 @@
isSystemProviderRequired = false,
isAutoSelectAllowed = false,
displayInfo,
- origin
+ origin,
+ preferImmediatelyAvailableCredentials,
) {
/**
@@ -53,15 +59,60 @@
* @param id the user id associated with the password
* @param password the password
* @param origin the origin of a different application if the request is being made on behalf of
- * that application. For API level >=34, setting a non-null value for this parameter, will throw
- * a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present.
+ * that application (Note: for API level >=34, setting a non-null value for this parameter will
+ * throw a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present)
+ * @param preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * immediately when there is no available password saving option instead of falling back
+ * to discovering remote options, and false (default) otherwise
* @throws NullPointerException If [id] is null
* @throws NullPointerException If [password] is null
* @throws IllegalArgumentException If [password] is empty
- * @throws SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present
+ * @throws SecurityException if [origin] is set but
+ * android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present
*/
- @JvmOverloads constructor(id: String, password: String, origin: String? = null) : this(id,
- password, DisplayInfo(id, null), origin)
+ @JvmOverloads constructor(
+ id: String,
+ password: String,
+ origin: String? = null,
+ preferImmediatelyAvailableCredentials: Boolean = false,
+ ) : this(id, password, DisplayInfo(id, null), origin, preferImmediatelyAvailableCredentials)
+
+ /**
+ * Constructs a [CreatePasswordRequest] to save the user password credential with their
+ * password provider.
+ *
+ * @param id the user id associated with the password
+ * @param password the password
+ * @param origin the origin of a different application if the request is being made on behalf of
+ * that application (Note: for API level >=34, setting a non-null value for this parameter will
+ * throw a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present)
+ * @param preferDefaultProvider the preferred default provider component name to prioritize in
+ * the selection UI flows (Note: your app must have the permission
+ * android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS to specify this, or it
+ * would not take effect; also this bit may not take effect for Android API level 33 and below,
+ * depending on the pre-34 provider(s) you have chosen)
+ * @param preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * immediately when there is no available passkey registration offering instead of falling back
+ * to discovering remote options, and false (preferably) otherwise
+ * @throws NullPointerException If [id] is null
+ * @throws NullPointerException If [password] is null
+ * @throws IllegalArgumentException If [password] is empty
+ * @throws SecurityException if [origin] is set but
+ * android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present
+ */
+ constructor(
+ id: String,
+ password: String,
+ origin: String?,
+ preferDefaultProvider: String?,
+ preferImmediatelyAvailableCredentials: Boolean,
+ ) : this(
+ id, password, DisplayInfo(
+ userId = id,
+ userDisplayName = null,
+ preferDefaultProvider = preferDefaultProvider,
+ ), origin, preferImmediatelyAvailableCredentials,
+ )
init {
require(password.isNotEmpty()) { "password should not be empty" }
@@ -89,16 +140,25 @@
@JvmStatic
@RequiresApi(23)
- internal fun createFrom(data: Bundle, origin: String? = null): CreatePasswordRequest {
+ internal fun createFrom(data: Bundle, origin: String?): CreatePasswordRequest {
try {
val id = data.getString(BUNDLE_KEY_ID)
val password = data.getString(BUNDLE_KEY_PASSWORD)
val displayInfo = DisplayInfo.parseFromCredentialDataBundle(data)
+ val preferImmediatelyAvailableCredentials =
+ data.getBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS, false)
return if (displayInfo == null) CreatePasswordRequest(
- id!!,
- password!!,
- origin
- ) else CreatePasswordRequest(id!!, password!!, displayInfo, origin)
+ id = id!!,
+ password = password!!,
+ origin = origin,
+ preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials,
+ ) else CreatePasswordRequest(
+ id = id!!,
+ password = password!!,
+ displayInfo = displayInfo,
+ origin = origin,
+ preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials,
+ )
} catch (e: Exception) {
throw FrameworkClassParsingException()
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt
index 366d9f1..b81ae9d 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt
@@ -25,37 +25,40 @@
/**
* A request to register a passkey from the user's public key credential provider.
*
+ * @property requestJson the request in JSON format in the [standard webauthn web json](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
+ * @property clientDataHash a clientDataHash value to sign over in place of assembling and hashing
+ * clientDataJSON during the signature request; only meaningful when [origin] is set
* @param requestJson the request in JSON format in the [standard webauthn web json](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
- * @param clientDataHash a hash that is used to verify the origin
+ * @param clientDataHash a clientDataHash value to sign over in place of assembling and hashing
+ * clientDataJSON during the signature request; only meaningful when [origin] is set
* @param preferImmediatelyAvailableCredentials true if you prefer the operation to return
* immediately when there is no available passkey registration offering instead of falling back to
* discovering remote options, and false (default) otherwise
* @param origin the origin of a different application if the request is being made on behalf of
- * that application. For API level >=34, setting a non-null value for this parameter, will throw
- * a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present.
+ * that application (Note: for API level >=34, setting a non-null value for this parameter will
+ * throw a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present)
*/
class CreatePublicKeyCredentialRequest private constructor(
val requestJson: String,
- val clientDataHash: String?,
- @get:JvmName("preferImmediatelyAvailableCredentials")
- val preferImmediatelyAvailableCredentials: Boolean,
+ val clientDataHash: ByteArray?,
+ preferImmediatelyAvailableCredentials: Boolean,
displayInfo: DisplayInfo,
origin: String? = null,
) : CreateCredentialRequest(
type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
- credentialData = toCredentialDataBundle(requestJson, clientDataHash,
- preferImmediatelyAvailableCredentials),
+ credentialData = toCredentialDataBundle(requestJson, clientDataHash),
// The whole request data should be passed during the query phase.
- candidateQueryData = toCredentialDataBundle(requestJson, clientDataHash,
- preferImmediatelyAvailableCredentials),
+ candidateQueryData = toCandidateDataBundle(requestJson, clientDataHash),
isSystemProviderRequired = false,
isAutoSelectAllowed = false,
displayInfo,
- origin
+ origin,
+ preferImmediatelyAvailableCredentials
) {
/**
- * Constructs a [CreatePublicKeyCredentialRequest] to register a passkey from the user's public key credential provider.
+ * Constructs a [CreatePublicKeyCredentialRequest] to register a passkey from the user's public
+ * key credential provider.
*
* @param requestJson the privileged request in JSON format in the [standard webauthn web json](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
* @param clientDataHash a hash that is used to verify the relying party identity
@@ -63,28 +66,58 @@
* immediately when there is no available passkey registration offering instead of falling back to
* discovering remote options, and false (default) otherwise
* @param origin the origin of a different application if the request is being made on behalf of
- * that application. For API level >=34, setting a non-null value for this parameter, will throw
- * a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present.
+ * that application (Note: for API level >=34, setting a non-null value for this parameter will
+ * throw a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present)
* @throws NullPointerException If [requestJson] is null
* @throws IllegalArgumentException If [requestJson] is empty, or if it doesn't have a valid
* `user.name` defined according to the [webauthn spec](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson)
*/
@JvmOverloads constructor(
requestJson: String,
- clientDataHash: String? = null,
+ clientDataHash: ByteArray? = null,
preferImmediatelyAvailableCredentials: Boolean = false,
origin: String? = null
) : this(requestJson, clientDataHash, preferImmediatelyAvailableCredentials,
getRequestDisplayInfo(requestJson), origin)
+ /**
+ * Constructs a [CreatePublicKeyCredentialRequest] to register a passkey from the user's public
+ * key credential provider.
+ *
+ * @param requestJson the privileged request in JSON format in the [standard webauthn web
+ * json](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
+ * @param clientDataHash a hash that is used to verify the relying party identity
+ * @param preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * immediately when there is no available passkey registration offering instead of falling back to
+ * discovering remote options, and false (preferably) otherwise
+ * @param origin the origin of a different application if the request is being made on behalf of
+ * that application (Note: for API level >=34, setting a non-null value for this parameter will
+ * throw a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present)
+ * @param preferDefaultProvider the preferred default provider component name to prioritize in
+ * the selection UI flows (Note: tour app must have the permission
+ * android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS to specify this, or it
+ * would not take effect; also this bit may not take effect for Android API level 33 and below,
+ * depending on the pre-34 provider(s) you have chosen)
+ * @throws NullPointerException If [requestJson] is null
+ * @throws IllegalArgumentException If [requestJson] is empty, or if it doesn't have a valid
+ * `user.name` defined according to the [webauthn
+ * spec](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson)
+ */
+ constructor(
+ requestJson: String,
+ clientDataHash: ByteArray?,
+ preferImmediatelyAvailableCredentials: Boolean,
+ origin: String?,
+ preferDefaultProvider: String?
+ ) : this(requestJson, clientDataHash, preferImmediatelyAvailableCredentials,
+ getRequestDisplayInfo(requestJson, preferDefaultProvider), origin)
+
init {
require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
}
/** @hide */
companion object {
- internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
- "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
internal const val BUNDLE_KEY_CLIENT_DATA_HASH =
"androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
@@ -92,14 +125,22 @@
"androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST"
@JvmStatic
- internal fun getRequestDisplayInfo(requestJson: String): DisplayInfo {
+ internal fun getRequestDisplayInfo(
+ requestJson: String,
+ defaultProvider: String? = null,
+ ): DisplayInfo {
return try {
val json = JSONObject(requestJson)
val user = json.getJSONObject("user")
val userName = user.getString("name")
val displayName: String? =
if (user.isNull("displayName")) null else user.getString("displayName")
- DisplayInfo(userName, displayName)
+ DisplayInfo(
+ userId = userName,
+ userDisplayName = displayName,
+ credentialTypeIcon = null,
+ preferDefaultProvider = defaultProvider,
+ )
} catch (e: Exception) {
throw IllegalArgumentException("user.name must be defined in requestJson")
}
@@ -108,8 +149,7 @@
@JvmStatic
internal fun toCredentialDataBundle(
requestJson: String,
- clientDataHash: String? = null,
- preferImmediatelyAvailableCredentials: Boolean
+ clientDataHash: ByteArray? = null,
): Bundle {
val bundle = Bundle()
bundle.putString(
@@ -117,19 +157,14 @@
BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST
)
bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
- bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
- bundle.putBoolean(
- BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
- preferImmediatelyAvailableCredentials
- )
+ bundle.putByteArray(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
return bundle
}
@JvmStatic
internal fun toCandidateDataBundle(
requestJson: String,
- clientDataHash: String?,
- preferImmediatelyAvailableCredentials: Boolean
+ clientDataHash: ByteArray?,
): Bundle {
val bundle = Bundle()
bundle.putString(
@@ -137,35 +172,29 @@
BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST
)
bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
- bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
- bundle.putBoolean(
- BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
- preferImmediatelyAvailableCredentials
- )
+ bundle.putByteArray(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
return bundle
}
- @Suppress("deprecation") // bundle.get() used for boolean value
- // to prevent default boolean value from being returned.
@JvmStatic
@RequiresApi(23)
- internal fun createFrom(data: Bundle, origin: String? = null):
+ internal fun createFrom(data: Bundle, origin: String?):
CreatePublicKeyCredentialRequest {
try {
val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
- val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
+ val clientDataHash = data.getByteArray(BUNDLE_KEY_CLIENT_DATA_HASH)
val preferImmediatelyAvailableCredentials =
- data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
+ data.getBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS, false)
val displayInfo = DisplayInfo.parseFromCredentialDataBundle(data)
return if (displayInfo == null) CreatePublicKeyCredentialRequest(
requestJson!!,
clientDataHash,
- (preferImmediatelyAvailableCredentials!!) as Boolean,
+ preferImmediatelyAvailableCredentials,
origin
) else CreatePublicKeyCredentialRequest(
requestJson!!,
clientDataHash,
- (preferImmediatelyAvailableCredentials!!) as Boolean,
+ preferImmediatelyAvailableCredentials,
displayInfo,
origin
)
diff --git a/credentials/credentials/src/main/java/androidx/credentials/Credential.kt b/credentials/credentials/src/main/java/androidx/credentials/Credential.kt
index 404ec2f..36d9558 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/Credential.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/Credential.kt
@@ -17,19 +17,19 @@
package androidx.credentials
import android.os.Bundle
-import androidx.annotation.RestrictTo
import androidx.credentials.internal.FrameworkClassParsingException
/**
* Base class for a credential with which the user consented to authenticate to the app.
+ *
+ * @property type the credential type determined by the credential-type-specific subclass (e.g.
+ * [PasswordCredential.TYPE_PASSWORD_CREDENTIAL] for `PasswordCredential` or
+ * [PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL] for `PublicKeyCredential`)
+ * @property data the credential data in the [Bundle] format
*/
abstract class Credential internal constructor(
- /** @hide */
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- open val type: String,
- /** @hide */
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- open val data: Bundle,
+ val type: String,
+ val data: Bundle,
) {
/** @hide */
companion object {
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialManager.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialManager.kt
index 53396d9..a73c71c 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialManager.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialManager.kt
@@ -16,15 +16,14 @@
package androidx.credentials
-import android.app.Activity
+import android.annotation.SuppressLint
+import android.app.PendingIntent
import android.content.Context
import android.os.CancellationSignal
+import androidx.annotation.RequiresApi
import androidx.credentials.exceptions.ClearCredentialException
-import androidx.credentials.exceptions.ClearCredentialProviderConfigurationException
import androidx.credentials.exceptions.CreateCredentialException
-import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException
import androidx.credentials.exceptions.GetCredentialException
-import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
import java.util.concurrent.Executor
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@@ -85,11 +84,17 @@
*
*
*/
-@Suppress("UNUSED_PARAMETER")
-class CredentialManager private constructor(private val context: Context) {
+@RequiresApi(16)
+@SuppressLint("ObsoleteSdkInt")
+interface CredentialManager {
companion object {
+ /**
+ * Creates a [CredentialManager] based on the given [context].
+ *
+ * @param context the context with which the CredentialManager should be associated
+ */
@JvmStatic
- fun create(context: Context): CredentialManager = CredentialManager(context)
+ fun create(context: Context): CredentialManager = CredentialManagerImpl(context)
}
/**
@@ -98,13 +103,14 @@
* The execution potentially launches framework UI flows for a user to view available
* credentials, consent to using one of them, etc.
*
+ * @param context the context used to launch any UI needed; use an activity context to make
+ * sure the UI will be launched within the same task stack
* @param request the request for getting the credential
- * @param activity the activity used to potentially launch any UI needed
* @throws GetCredentialException If the request fails
*/
suspend fun getCredential(
+ context: Context,
request: GetCredentialRequest,
- activity: Activity,
): GetCredentialResponse = suspendCancellableCoroutine { continuation ->
// Any Android API that supports cancellation should be configured to propagate
// coroutine cancellation as follows:
@@ -123,8 +129,97 @@
}
getCredentialAsync(
+ context,
request,
- activity,
+ canceller,
+ // Use a direct executor to avoid extra dispatch. Resuming the continuation will
+ // handle getting to the right thread or pool via the ContinuationInterceptor.
+ Runnable::run,
+ callback)
+ }
+
+ /**
+ * Requests a credential from the user.
+ *
+ * Different from the other `getCredential(GetCredentialRequest, Activity)` API, this API
+ * launches the remaining flows to retrieve an app credential from the user, after the
+ * completed prefetch work corresponding to the given `pendingGetCredentialHandle`. Use this
+ * API to complete the full credential retrieval operation after you initiated a request through
+ * the [prepareGetCredential] API.
+ *
+ * The execution can potentially launch UI flows to collect user consent to using a
+ * credential, display a picker when multiple credentials exist, etc.
+ *
+ * @param context the context used to launch any UI needed; use an activity context to make
+ * sure the UI will be launched within the same task stack
+ * @param pendingGetCredentialHandle the handle representing the pending operation to resume
+ * @throws GetCredentialException If the request fails
+ */
+ @RequiresApi(34)
+ suspend fun getCredential(
+ context: Context,
+ pendingGetCredentialHandle: PrepareGetCredentialResponse.PendingGetCredentialHandle,
+ ): GetCredentialResponse = suspendCancellableCoroutine { continuation ->
+ // Any Android API that supports cancellation should be configured to propagate
+ // coroutine cancellation as follows:
+ val canceller = CancellationSignal()
+ continuation.invokeOnCancellation { canceller.cancel() }
+
+ val callback = object : CredentialManagerCallback<GetCredentialResponse,
+ GetCredentialException> {
+ override fun onResult(result: GetCredentialResponse) {
+ continuation.resume(result)
+ }
+
+ override fun onError(e: GetCredentialException) {
+ continuation.resumeWithException(e)
+ }
+ }
+
+ getCredentialAsync(
+ context,
+ pendingGetCredentialHandle,
+ canceller,
+ // Use a direct executor to avoid extra dispatch. Resuming the continuation will
+ // handle getting to the right thread or pool via the ContinuationInterceptor.
+ Runnable::run,
+ callback)
+ }
+
+ /**
+ * Prepares for a get-credential operation. Returns a [PrepareGetCredentialResponse]
+ * that can later be used to launch the credential retrieval UI flow to finalize a user
+ * credential for your app.
+ *
+ * This API doesn't invoke any UI. It only performs the preparation work so that you can
+ * later launch the remaining get-credential operation (involves UIs) through the
+ * [getCredential] API which incurs less latency than executing the whole operation in one call.
+ *
+ * @param request the request for getting the credential
+ * @throws GetCredentialException If the request fails
+ */
+ @RequiresApi(34)
+ suspend fun prepareGetCredential(
+ request: GetCredentialRequest,
+ ): PrepareGetCredentialResponse = suspendCancellableCoroutine { continuation ->
+ // Any Android API that supports cancellation should be configured to propagate
+ // coroutine cancellation as follows:
+ val canceller = CancellationSignal()
+ continuation.invokeOnCancellation { canceller.cancel() }
+
+ val callback = object : CredentialManagerCallback<PrepareGetCredentialResponse,
+ GetCredentialException> {
+ override fun onResult(result: PrepareGetCredentialResponse) {
+ continuation.resume(result)
+ }
+
+ override fun onError(e: GetCredentialException) {
+ continuation.resumeWithException(e)
+ }
+ }
+
+ prepareGetCredentialAsync(
+ request,
canceller,
// Use a direct executor to avoid extra dispatch. Resuming the continuation will
// handle getting to the right thread or pool via the ContinuationInterceptor.
@@ -139,13 +234,14 @@
* The execution potentially launches framework UI flows for a user to view their registration
* options, grant consent, etc.
*
+ * @param context the context used to launch any UI needed; use an activity context to make
+ * sure the UI will be launched within the same task stack
* @param request the request for creating the credential
- * @param activity the activity used to potentially launch any UI needed
* @throws CreateCredentialException If the request fails
*/
suspend fun createCredential(
+ context: Context,
request: CreateCredentialRequest,
- activity: Activity,
): CreateCredentialResponse = suspendCancellableCoroutine { continuation ->
// Any Android API that supports cancellation should be configured to propagate
// coroutine cancellation as follows:
@@ -164,8 +260,8 @@
}
createCredentialAsync(
+ context,
request,
- activity,
canceller,
// Use a direct executor to avoid extra dispatch. Resuming the continuation will
// handle getting to the right thread or pool via the ContinuationInterceptor.
@@ -216,73 +312,112 @@
}
/**
- * Java API for requesting a credential from the user.
+ * Requests a credential from the user.
+ *
+ * This API uses callbacks instead of Kotlin coroutines.
*
* The execution potentially launches framework UI flows for a user to view available
* credentials, consent to using one of them, etc.
*
+ * @param context the context used to launch any UI needed; use an activity context to make
+ * sure the UI will be launched within the same task stack
* @param request the request for getting the credential
- * @param activity an optional activity used to potentially launch any UI needed
* @param cancellationSignal an optional signal that allows for cancelling this call
* @param executor the callback will take place on this executor
* @param callback the callback invoked when the request succeeds or fails
*/
fun getCredentialAsync(
+ context: Context,
request: GetCredentialRequest,
- activity: Activity,
cancellationSignal: CancellationSignal?,
executor: Executor,
callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>,
- ) {
- val provider: CredentialProvider? = CredentialProviderFactory
- .getBestAvailableProvider(context)
- if (provider == null) {
- // TODO (Update with the right error code when ready)
- callback.onError(
- GetCredentialProviderConfigurationException(
- "getCredentialAsync no provider dependencies found - please ensure " +
- "the desired provider dependencies are added")
- )
- return
- }
- provider.onGetCredential(request, activity, cancellationSignal, executor, callback)
- }
+ )
/**
- * Java API for registering a user credential that can be used to authenticate the user to
+ * Requests a credential from the user.
+ *
+ * This API uses callbacks instead of Kotlin coroutines.
+ *
+ * Different from the other `getCredentialAsync(GetCredentialRequest, Activity)` API, this API
+ * launches the remaining flows to retrieve an app credential from the user, after the
+ * completed prefetch work corresponding to the given `pendingGetCredentialHandle`. Use this
+ * API to complete the full credential retrieval operation after you initiated a request through
+ * the [prepareGetCredentialAsync] API.
+ *
+ * The execution can potentially launch UI flows to collect user consent to using a
+ * credential, display a picker when multiple credentials exist, etc.
+ *
+ * @param context the context used to launch any UI needed; use an activity context to make
+ * sure the UI will be launched within the same task stack
+ * @param pendingGetCredentialHandle the handle representing the pending operation to resume
+ * @param cancellationSignal an optional signal that allows for cancelling this call
+ * @param executor the callback will take place on this executor
+ * @param callback the callback invoked when the request succeeds or fails
+ */
+ @RequiresApi(34)
+ fun getCredentialAsync(
+ context: Context,
+ pendingGetCredentialHandle: PrepareGetCredentialResponse.PendingGetCredentialHandle,
+ cancellationSignal: CancellationSignal?,
+ executor: Executor,
+ callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>,
+ )
+
+ /**
+ * Prepares for a get-credential operation. Returns a [PrepareGetCredentialResponse]
+ * that can later be used to launch the credential retrieval UI flow to finalize a user
+ * credential for your app.
+ *
+ * This API uses callbacks instead of Kotlin coroutines.
+ *
+ * This API doesn't invoke any UI. It only performs the preparation work so that you can
+ * later launch the remaining get-credential operation (involves UIs) through the
+ * [getCredentialAsync] API which incurs less latency than executing the whole operation in one
+ * call.
+ *
+ * @param request the request for getting the credential
+ * @param cancellationSignal an optional signal that allows for cancelling this call
+ * @param executor the callback will take place on this executor
+ * @param callback the callback invoked when the request succeeds or fails
+ */
+ @RequiresApi(34)
+ fun prepareGetCredentialAsync(
+ request: GetCredentialRequest,
+ cancellationSignal: CancellationSignal?,
+ executor: Executor,
+ callback: CredentialManagerCallback<PrepareGetCredentialResponse, GetCredentialException>,
+ )
+
+ /**
+ * Registers a user credential that can be used to authenticate the user to
* the app in the future.
*
+ * This API uses callbacks instead of Kotlin coroutines.
+ *
* The execution potentially launches framework UI flows for a user to view their registration
* options, grant consent, etc.
*
+ * @param context the context used to launch any UI needed; use an activity context to make
+ * sure the UI will be launched within the same task stack
* @param request the request for creating the credential
- * @param activity an optional activity used to potentially launch any UI needed
* @param cancellationSignal an optional signal that allows for cancelling this call
* @param executor the callback will take place on this executor
* @param callback the callback invoked when the request succeeds or fails
*/
fun createCredentialAsync(
+ context: Context,
request: CreateCredentialRequest,
- activity: Activity,
cancellationSignal: CancellationSignal?,
executor: Executor,
callback: CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>,
- ) {
- val provider: CredentialProvider? = CredentialProviderFactory
- .getBestAvailableProvider(context)
- if (provider == null) {
- // TODO (Update with the right error code when ready)
- callback.onError(CreateCredentialProviderConfigurationException(
- "createCredentialAsync no provider dependencies found - please ensure the " +
- "desired provider dependencies are added"))
- return
- }
- provider.onCreateCredential(request, activity, cancellationSignal, executor, callback)
- }
+ )
/**
* Clears the current user credential state from all credential providers.
*
+ * This API uses callbacks instead of Kotlin coroutines.
+ *
* You should invoked this api after your user signs out of your app to notify all credential
* providers that any stored credential session for the given app should be cleared.
*
@@ -302,16 +437,12 @@
cancellationSignal: CancellationSignal?,
executor: Executor,
callback: CredentialManagerCallback<Void?, ClearCredentialException>,
- ) {
- val provider: CredentialProvider? = CredentialProviderFactory
- .getBestAvailableProvider(context)
- if (provider == null) {
- // TODO (Update with the right error code when ready)
- callback.onError(ClearCredentialProviderConfigurationException(
- "clearCredentialStateAsync no provider dependencies found - please ensure the " +
- "desired provider dependencies are added"))
- return
- }
- provider.onClearCredential(request, cancellationSignal, executor, callback)
- }
+ )
+
+ /**
+ * Returns a pending intent that shows a screen that lets a user enable a Credential Manager provider.
+ * @return the pending intent that can be launched
+ */
+ @RequiresApi(34)
+ fun createSettingsPendingIntent(): PendingIntent
}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialManagerImpl.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialManagerImpl.kt
new file mode 100644
index 0000000..1b0b198
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialManagerImpl.kt
@@ -0,0 +1,283 @@
+/*
+ * 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.credentials
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.CancellationSignal
+import androidx.annotation.RequiresApi
+import androidx.credentials.exceptions.ClearCredentialException
+import androidx.credentials.exceptions.ClearCredentialProviderConfigurationException
+import androidx.credentials.exceptions.CreateCredentialException
+import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException
+import androidx.credentials.exceptions.GetCredentialException
+import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
+import java.util.concurrent.Executor
+
+/**
+ * Manages user authentication flows.
+ *
+ * An application can call the CredentialManager apis to launch framework UI flows for a user to
+ * register a new credential or to consent to a saved credential from supported credential
+ * providers, which can then be used to authenticate to the app.
+ *
+ * This class contains its own exception types.
+ * They represent unique failures during the Credential Manager flow. As required, they
+ * can be extended for unique types containing new and unique versions of the exception - either
+ * with new 'exception types' (same credential class, different exceptions), or inner subclasses
+ * and their exception types (a subclass credential class and all their exception types).
+ *
+ * For example, if there is an UNKNOWN exception type, assuming the base Exception is
+ * [ClearCredentialException], we can add an 'exception type' class for it as follows:
+ * TODO("Add in new flow with extensive 'getType' function")
+ * ```
+ * class ClearCredentialUnknownException(
+ * errorMessage: CharSequence? = null
+ * ) : ClearCredentialException(TYPE_CLEAR_CREDENTIAL_UNKNOWN_EXCEPTION, errorMessage) {
+ * // ...Any required impl here...//
+ * companion object {
+ * private const val TYPE_CLEAR_CREDENTIAL_UNKNOWN_EXCEPTION: String =
+ * "androidx.credentials.TYPE_CLEAR_CREDENTIAL_UNKNOWN_EXCEPTION"
+ * }
+ * }
+ * ```
+ *
+ * Furthermore, the base class can be subclassed to a new more specific credential type, which
+ * then can further be subclassed into individual exception types. The first is an example of a
+ * 'inner credential type exception', and the next is a 'exception type' of this subclass exception.
+ *
+ * ```
+ * class UniqueCredentialBasedOnClearCredentialException(
+ * type: String,
+ * errorMessage: CharSequence? = null
+ * ) : ClearCredentialException(type, errorMessage) {
+ * // ... Any required impl here...//
+ * }
+ * // .... code and logic .... //
+ * class UniqueCredentialBasedOnClearCredentialUnknownException(
+ * errorMessage: CharSequence? = null
+ * ) : ClearCredentialException(TYPE_UNIQUE_CREDENTIAL_BASED_ON_CLEAR_CREDENTIAL_UNKNOWN_EXCEPTION,
+ * errorMessage) {
+ * // ... Any required impl here ... //
+ * companion object {
+ * private const val
+ * TYPE_UNIQUE_CREDENTIAL_BASED_ON_CLEAR_CREDENTIAL_UNKNOWN_EXCEPTION: String =
+ * "androidx.credentials.TYPE_CLEAR_CREDENTIAL_UNKNOWN_EXCEPTION"
+ * }
+ * }
+ * ```
+ *
+ *
+ */
+@RequiresApi(16)
+@SuppressLint("ObsoleteSdkInt")
+internal class CredentialManagerImpl internal constructor(
+ private val context: Context
+) : CredentialManager {
+ companion object {
+ /**
+ * An intent action that shows a screen that let user enable a Credential Manager provider.
+ */
+ private const val
+ INTENT_ACTION_FOR_CREDENTIAL_PROVIDER_SETTINGS: String =
+ "android.settings.CREDENTIAL_PROVIDER"
+ }
+
+ /**
+ * Requests a credential from the user.
+ *
+ * This API uses callbacks instead of Kotlin coroutines.
+ *
+ * The execution potentially launches framework UI flows for a user to view available
+ * credentials, consent to using one of them, etc.
+ *
+ * @param context the context used to launch any UI needed; use an activity context to make
+ * sure the UI will be launched within the same task stack
+ * @param request the request for getting the credential
+ * @param cancellationSignal an optional signal that allows for cancelling this call
+ * @param executor the callback will take place on this executor
+ * @param callback the callback invoked when the request succeeds or fails
+ */
+ override fun getCredentialAsync(
+ context: Context,
+ request: GetCredentialRequest,
+ cancellationSignal: CancellationSignal?,
+ executor: Executor,
+ callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>,
+ ) {
+ val provider: CredentialProvider? = CredentialProviderFactory
+ .getBestAvailableProvider(this.context)
+ if (provider == null) {
+ // TODO (Update with the right error code when ready)
+ callback.onError(
+ GetCredentialProviderConfigurationException(
+ "getCredentialAsync no provider dependencies found - please ensure " +
+ "the desired provider dependencies are added")
+ )
+ return
+ }
+ provider.onGetCredential(context, request, cancellationSignal, executor, callback)
+ }
+
+ /**
+ * Requests a credential from the user.
+ *
+ * This API uses callbacks instead of Kotlin coroutines.
+ *
+ * Different from the other `getCredentialAsync(GetCredentialRequest, Activity)` API, this API
+ * launches the remaining flows to retrieve an app credential from the user, after the
+ * completed prefetch work corresponding to the given `pendingGetCredentialHandle`. Use this
+ * API to complete the full credential retrieval operation after you initiated a request through
+ * the [prepareGetCredentialAsync] API.
+ *
+ * The execution can potentially launch UI flows to collect user consent to using a
+ * credential, display a picker when multiple credentials exist, etc.
+ *
+ * @param context the context used to launch any UI needed; use an activity context to make
+ * sure the UI will be launched within the same task stack
+ * @param pendingGetCredentialHandle the handle representing the pending operation to resume
+ * @param cancellationSignal an optional signal that allows for cancelling this call
+ * @param executor the callback will take place on this executor
+ * @param callback the callback invoked when the request succeeds or fails
+ */
+ @RequiresApi(34)
+ override fun getCredentialAsync(
+ context: Context,
+ pendingGetCredentialHandle: PrepareGetCredentialResponse.PendingGetCredentialHandle,
+ cancellationSignal: CancellationSignal?,
+ executor: Executor,
+ callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>,
+ ) {
+ val provider = CredentialProviderFactory.getUAndAboveProvider(context)
+ provider.onGetCredential(
+ context, pendingGetCredentialHandle, cancellationSignal, executor, callback)
+ }
+
+ /**
+ * Prepares for a get-credential operation. Returns a [PrepareGetCredentialResponse]
+ * that can later be used to launch the credential retrieval UI flow to finalize a user
+ * credential for your app.
+ *
+ * This API uses callbacks instead of Kotlin coroutines.
+ *
+ * This API doesn't invoke any UI. It only performs the preparation work so that you can
+ * later launch the remaining get-credential operation (involves UIs) through the
+ * [getCredentialAsync] API which incurs less latency than executing the whole operation in one
+ * call.
+ *
+ * @param request the request for getting the credential
+ * @param cancellationSignal an optional signal that allows for cancelling this call
+ * @param executor the callback will take place on this executor
+ * @param callback the callback invoked when the request succeeds or fails
+ */
+ @RequiresApi(34)
+ override fun prepareGetCredentialAsync(
+ request: GetCredentialRequest,
+ cancellationSignal: CancellationSignal?,
+ executor: Executor,
+ callback: CredentialManagerCallback<PrepareGetCredentialResponse, GetCredentialException>,
+ ) {
+ val provider = CredentialProviderFactory.getUAndAboveProvider(context)
+ provider.onPrepareCredential(request, cancellationSignal, executor, callback)
+ }
+
+ /**
+ * Registers a user credential that can be used to authenticate the user to
+ * the app in the future.
+ *
+ * This API uses callbacks instead of Kotlin coroutines.
+ *
+ * The execution potentially launches framework UI flows for a user to view their registration
+ * options, grant consent, etc.
+ *
+ * @param context the context used to launch any UI needed; use an activity context to make
+ * sure the UI will be launched within the same task stack
+ * @param request the request for creating the credential
+ * @param cancellationSignal an optional signal that allows for cancelling this call
+ * @param executor the callback will take place on this executor
+ * @param callback the callback invoked when the request succeeds or fails
+ */
+ override fun createCredentialAsync(
+ context: Context,
+ request: CreateCredentialRequest,
+ cancellationSignal: CancellationSignal?,
+ executor: Executor,
+ callback: CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>,
+ ) {
+ val provider: CredentialProvider? = CredentialProviderFactory
+ .getBestAvailableProvider(this.context)
+ if (provider == null) {
+ // TODO (Update with the right error code when ready)
+ callback.onError(CreateCredentialProviderConfigurationException(
+ "createCredentialAsync no provider dependencies found - please ensure the " +
+ "desired provider dependencies are added"))
+ return
+ }
+ provider.onCreateCredential(context, request, cancellationSignal, executor, callback)
+ }
+
+ /**
+ * Clears the current user credential state from all credential providers.
+ *
+ * This API uses callbacks instead of Kotlin coroutines.
+ *
+ * You should invoked this api after your user signs out of your app to notify all credential
+ * providers that any stored credential session for the given app should be cleared.
+ *
+ * A credential provider may have stored an active credential session and use it to limit
+ * sign-in options for future get-credential calls. For example, it may prioritize the active
+ * credential over any other available credential. When your user explicitly signs out of your
+ * app and in order to get the holistic sign-in options the next time, you should call this API
+ * to let the provider clear any stored credential session.
+ *
+ * @param request the request for clearing the app user's credential state
+ * @param cancellationSignal an optional signal that allows for cancelling this call
+ * @param executor the callback will take place on this executor
+ * @param callback the callback invoked when the request succeeds or fails
+ */
+ override fun clearCredentialStateAsync(
+ request: ClearCredentialStateRequest,
+ cancellationSignal: CancellationSignal?,
+ executor: Executor,
+ callback: CredentialManagerCallback<Void?, ClearCredentialException>,
+ ) {
+ val provider: CredentialProvider? = CredentialProviderFactory
+ .getBestAvailableProvider(context)
+ if (provider == null) {
+ // TODO (Update with the right error code when ready)
+ callback.onError(ClearCredentialProviderConfigurationException(
+ "clearCredentialStateAsync no provider dependencies found - please ensure the " +
+ "desired provider dependencies are added"))
+ return
+ }
+ provider.onClearCredential(request, cancellationSignal, executor, callback)
+ }
+
+ /**
+ * Returns a pending intent that shows a screen that lets a user enable a Credential Manager provider.
+ * @return the pending intent that can be launched
+ */
+ @RequiresApi(34)
+ override fun createSettingsPendingIntent(): PendingIntent {
+ val intent: Intent = Intent(INTENT_ACTION_FOR_CREDENTIAL_PROVIDER_SETTINGS)
+ intent.setData(Uri.parse("package:" + context.getPackageName()))
+ return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt
index 4655ca9..9a1b6ad 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt
@@ -16,8 +16,8 @@
package androidx.credentials
+import android.content.ComponentName
import android.os.Bundle
-import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.credentials.internal.FrameworkClassParsingException
@@ -26,30 +26,36 @@
*
* [GetCredentialRequest] will be composed of a list of [CredentialOption] subclasses to indicate
* the specific credential types and configurations that your app accepts.
+ *
+ * @property type the credential type determined by the credential-type-specific subclass (e.g.
+ * the type for [GetPasswordOption] is [PasswordCredential.TYPE_PASSWORD_CREDENTIAL] and for
+ * [GetPublicKeyCredentialOption] is [PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL])
+ * @property requestData the request data in the [Bundle] format
+ * @property candidateQueryData the partial request data in the [Bundle] format that will be sent to
+ * the provider during the initial candidate query stage, which will not contain sensitive user
+ * information
+ * @property isSystemProviderRequired true if must only be fulfilled by a system provider and false
+ * otherwise
+ * @property isAutoSelectAllowed whether a credential entry will be automatically chosen if it is
+ * the only one available option
+ * @property allowedProviders a set of provider service [ComponentName] allowed to receive this
+ * option (Note: a [SecurityException] will be thrown if it is set as non-empty but your app does
+ * not have android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS; for API level < 34,
+ * this property will not take effect and you should control the allowed provider via
+ * [library dependencies](https://developer.android.com/training/sign-in/passkeys#add-dependencies))
*/
abstract class CredentialOption internal constructor(
- /** @hide */
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- open val type: String,
- /** @hide */
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- open val requestData: Bundle,
- /** @hide */
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- open val candidateQueryData: Bundle,
- /** @hide */
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- open val isSystemProviderRequired: Boolean,
- /** @hide */
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- open val isAutoSelectAllowed: Boolean,
+ val type: String,
+ val requestData: Bundle,
+ val candidateQueryData: Bundle,
+ val isSystemProviderRequired: Boolean,
+ val isAutoSelectAllowed: Boolean,
+ val allowedProviders: Set<ComponentName>,
) {
init {
- @Suppress("UNNECESSARY_SAFE_CALL")
- requestData?.let {
- it.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, isAutoSelectAllowed)
- }
+ requestData.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, isAutoSelectAllowed)
+ candidateQueryData.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, isAutoSelectAllowed)
}
/** @hide */
@@ -59,23 +65,29 @@
const val BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED =
"androidx.credentials.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED"
+ internal fun extractAutoSelectValue(data: Bundle): Boolean {
+ return data.getBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED)
+ }
+
/** @hide */
@JvmStatic
fun createFrom(
type: String,
requestData: Bundle,
candidateQueryData: Bundle,
- requireSystemProvider: Boolean
+ requireSystemProvider: Boolean,
+ allowedProviders: Set<ComponentName>,
): CredentialOption {
return try {
when (type) {
PasswordCredential.TYPE_PASSWORD_CREDENTIAL ->
- GetPasswordOption.createFrom(requestData)
+ GetPasswordOption.createFrom(requestData, allowedProviders)
PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
when (requestData.getString(PublicKeyCredential.BUNDLE_KEY_SUBTYPE)) {
GetPublicKeyCredentialOption
.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION ->
- GetPublicKeyCredentialOption.createFrom(requestData)
+ GetPublicKeyCredentialOption.createFrom(
+ requestData, allowedProviders)
else -> throw FrameworkClassParsingException()
}
else -> throw FrameworkClassParsingException()
@@ -84,11 +96,13 @@
// Parsing failed but don't crash the process. Instead just output a request with
// the raw framework values.
GetCustomCredentialOption(
- type,
- requestData,
- candidateQueryData,
- requireSystemProvider,
- requestData.getBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, false)
+ type = type,
+ requestData = requestData,
+ candidateQueryData = candidateQueryData,
+ isSystemProviderRequired = requireSystemProvider,
+ isAutoSelectAllowed = requestData.getBoolean(
+ BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, false),
+ allowedProviders = allowedProviders,
)
}
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialProvider.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialProvider.kt
index 49085d8..1dab141 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialProvider.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialProvider.kt
@@ -16,8 +16,9 @@
package androidx.credentials
-import android.app.Activity
+import android.content.Context
import android.os.CancellationSignal
+import androidx.annotation.RequiresApi
import androidx.credentials.exceptions.ClearCredentialException
import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.GetCredentialException
@@ -50,15 +51,15 @@
/**
* Invoked on a request to get a credential.
*
+ * @param context the client calling context used to potentially launch any UI needed
* @param request the request for getting the credential
- * @param activity the client calling activity used to potentially launch any UI needed
* @param cancellationSignal an optional signal that allows for cancelling this call
* @param executor the callback will take place on this executor
* @param callback the callback invoked when the request succeeds or fails
*/
fun onGetCredential(
+ context: Context,
request: GetCredentialRequest,
- activity: Activity,
cancellationSignal: CancellationSignal?,
executor: Executor,
callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>,
@@ -67,15 +68,15 @@
/**
* Invoked on a request to create a credential.
*
+ * @param context the client calling context used to potentially launch any UI needed
* @param request the request for creating the credential
- * @param activity the client calling activity used to potentially launch any UI needed
* @param cancellationSignal an optional signal that allows for cancelling this call
* @param executor the callback will take place on this executor
* @param callback the callback invoked when the request succeeds or fails
*/
fun onCreateCredential(
+ context: Context,
request: CreateCredentialRequest,
- activity: Activity,
cancellationSignal: CancellationSignal?,
executor: Executor,
callback: CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>,
@@ -98,4 +99,38 @@
executor: Executor,
callback: CredentialManagerCallback<Void?, ClearCredentialException>,
)
+
+ /**
+ * Invoked on a request to prepare for a get-credential operation
+ *
+ * @param request the request for getting the credential
+ * @param cancellationSignal an optional signal that allows for cancelling this call
+ * @param executor the callback will take place on this executor
+ * @param callback the callback invoked when the request succeeds or fails
+ */
+ @RequiresApi(34)
+ fun onPrepareCredential(
+ request: GetCredentialRequest,
+ cancellationSignal: CancellationSignal?,
+ executor: Executor,
+ callback: CredentialManagerCallback<PrepareGetCredentialResponse, GetCredentialException>,
+ ) {}
+
+ /**
+ * Complete on a request to get a credential represented by the [pendingGetCredentialHandle].
+ *
+ * @param context the client calling context used to potentially launch any UI needed
+ * @param pendingGetCredentialHandle the handle representing the pending operation to resume
+ * @param cancellationSignal an optional signal that allows for cancelling this call
+ * @param executor the callback will take place on this executor
+ * @param callback the callback invoked when the request succeeds or fails
+ */
+ @RequiresApi(34)
+ fun onGetCredential(
+ context: Context,
+ pendingGetCredentialHandle: PrepareGetCredentialResponse.PendingGetCredentialHandle,
+ cancellationSignal: CancellationSignal?,
+ executor: Executor,
+ callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>,
+ ) {}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
index cb5b9f9..662acf8 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
@@ -18,7 +18,11 @@
import android.content.Context
import android.content.pm.PackageManager
+import android.os.Build
import android.util.Log
+import androidx.annotation.OptIn
+import androidx.annotation.RequiresApi
+import androidx.core.os.BuildCompat
/**
* Factory that returns the credential provider to be used by Credential Manager.
@@ -28,6 +32,7 @@
class CredentialProviderFactory {
companion object {
private const val TAG = "CredProviderFactory"
+ private const val MAX_CRED_MAN_PRE_FRAMEWORK_API_LEVEL = Build.VERSION_CODES.TIRAMISU
/** The metadata key to be used when specifying the provider class name in the
* android manifest file. */
@@ -39,8 +44,20 @@
* the app. Developer must not add more than one provider library.
* Post-U, providers will be registered with the framework, and enabled by the user.
*/
+ @OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
fun getBestAvailableProvider(context: Context): CredentialProvider? {
- return tryCreatePreUOemProvider(context)
+ if (BuildCompat.isAtLeastU()) {
+ return CredentialProviderFrameworkImpl(context)
+ } else if (Build.VERSION.SDK_INT <= MAX_CRED_MAN_PRE_FRAMEWORK_API_LEVEL) {
+ return tryCreatePreUOemProvider(context)
+ } else {
+ return null
+ }
+ }
+
+ @RequiresApi(34)
+ fun getUAndAboveProvider(context: Context): CredentialProvider {
+ return CredentialProviderFrameworkImpl(context)
}
private fun tryCreatePreUOemProvider(context: Context): CredentialProvider? {
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFrameworkImpl.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFrameworkImpl.kt
new file mode 100644
index 0000000..2b934be
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFrameworkImpl.kt
@@ -0,0 +1,379 @@
+/*
+ * 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.credentials
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.credentials.CredentialManager
+import android.os.Bundle
+import android.os.CancellationSignal
+import android.os.OutcomeReceiver
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.credentials.exceptions.ClearCredentialException
+import androidx.credentials.exceptions.ClearCredentialUnknownException
+import androidx.credentials.exceptions.ClearCredentialUnsupportedException
+import androidx.credentials.exceptions.CreateCredentialCancellationException
+import androidx.credentials.exceptions.CreateCredentialException
+import androidx.credentials.exceptions.CreateCredentialInterruptedException
+import androidx.credentials.exceptions.CreateCredentialNoCreateOptionException
+import androidx.credentials.exceptions.CreateCredentialUnknownException
+import androidx.credentials.exceptions.CreateCredentialUnsupportedException
+import androidx.credentials.exceptions.GetCredentialCancellationException
+import androidx.credentials.exceptions.GetCredentialException
+import androidx.credentials.exceptions.GetCredentialInterruptedException
+import androidx.credentials.exceptions.GetCredentialUnknownException
+import androidx.credentials.exceptions.GetCredentialUnsupportedException
+import androidx.credentials.exceptions.NoCredentialException
+import androidx.credentials.internal.FrameworkImplHelper
+import java.util.concurrent.Executor
+
+/**
+ * Framework credential provider implementation that allows credential
+ * manager requests to be routed to the framework.
+ *
+ * @hide
+ */
+@RequiresApi(34)
+class CredentialProviderFrameworkImpl(context: Context) : CredentialProvider {
+ private val credentialManager: CredentialManager? =
+ context.getSystemService(Context.CREDENTIAL_SERVICE) as CredentialManager?
+
+ override fun onPrepareCredential(
+ request: GetCredentialRequest,
+ cancellationSignal: CancellationSignal?,
+ executor: Executor,
+ callback: CredentialManagerCallback<PrepareGetCredentialResponse, GetCredentialException>
+ ) {
+ if (isCredmanDisabled {
+ callback.onError(
+ GetCredentialUnsupportedException(
+ "Your device doesn't support credential manager"
+ )
+ )
+ }) return
+ val outcome = object : OutcomeReceiver<
+ android.credentials.PrepareGetCredentialResponse,
+ android.credentials.GetCredentialException> {
+ override fun onResult(response: android.credentials.PrepareGetCredentialResponse) {
+ callback.onResult(convertPrepareGetResponseToJetpackClass(response))
+ }
+
+ override fun onError(error: android.credentials.GetCredentialException) {
+ callback.onError(convertToJetpackGetException(error))
+ }
+ }
+
+ credentialManager!!.prepareGetCredential(
+ convertGetRequestToFrameworkClass(request),
+ cancellationSignal,
+ executor,
+ outcome
+ )
+ }
+
+ override fun onGetCredential(
+ context: Context,
+ pendingGetCredentialHandle: PrepareGetCredentialResponse.PendingGetCredentialHandle,
+ cancellationSignal: CancellationSignal?,
+ executor: Executor,
+ callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>
+ ) {
+ if (isCredmanDisabled {
+ callback.onError(
+ GetCredentialUnsupportedException(
+ "Your device doesn't support credential manager"
+ )
+ )
+ }) return
+ val outcome = object : OutcomeReceiver<
+ android.credentials.GetCredentialResponse, android.credentials.GetCredentialException> {
+ override fun onResult(response: android.credentials.GetCredentialResponse) {
+ callback.onResult(convertGetResponseToJetpackClass(response))
+ }
+
+ override fun onError(error: android.credentials.GetCredentialException) {
+ callback.onError(convertToJetpackGetException(error))
+ }
+ }
+
+ credentialManager!!.getCredential(
+ context,
+ pendingGetCredentialHandle.frameworkHandle!!,
+ cancellationSignal,
+ executor,
+ outcome
+ )
+ }
+
+ override fun onGetCredential(
+ context: Context,
+ request: GetCredentialRequest,
+ cancellationSignal: CancellationSignal?,
+ executor: Executor,
+ callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>
+ ) {
+ if (isCredmanDisabled {
+ callback.onError(
+ GetCredentialUnsupportedException(
+ "Your device doesn't support credential manager"
+ )
+ )
+ }) return
+
+ val outcome = object : OutcomeReceiver<
+ android.credentials.GetCredentialResponse, android.credentials.GetCredentialException> {
+ override fun onResult(response: android.credentials.GetCredentialResponse) {
+ Log.i(TAG, "GetCredentialResponse returned from framework")
+ callback.onResult(convertGetResponseToJetpackClass(response))
+ }
+
+ override fun onError(error: android.credentials.GetCredentialException) {
+ Log.i(TAG, "GetCredentialResponse error returned from framework")
+ callback.onError(convertToJetpackGetException(error))
+ }
+ }
+
+ credentialManager!!.getCredential(
+ context,
+ convertGetRequestToFrameworkClass(request),
+ cancellationSignal,
+ executor,
+ outcome
+ )
+ }
+
+ private fun isCredmanDisabled(handleNullCredMan: () -> Unit): Boolean {
+ if (credentialManager == null) {
+ handleNullCredMan()
+ return true
+ }
+ return false
+ }
+
+ override fun onCreateCredential(
+ context: Context,
+ request: CreateCredentialRequest,
+ cancellationSignal: CancellationSignal?,
+ executor: Executor,
+ callback: CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>
+ ) {
+ if (isCredmanDisabled {
+ callback.onError(
+ CreateCredentialUnsupportedException(
+ "Your device doesn't support credential manager"
+ )
+ )
+ }) return
+ val outcome = object : OutcomeReceiver<
+ android.credentials.CreateCredentialResponse,
+ android.credentials.CreateCredentialException> {
+ override fun onResult(response: android.credentials.CreateCredentialResponse) {
+ Log.i(TAG, "Create Result returned from framework: ")
+ callback.onResult(
+ CreateCredentialResponse.createFrom(
+ request.type, response.data
+ )
+ )
+ }
+
+ override fun onError(error: android.credentials.CreateCredentialException) {
+ Log.i(TAG, "CreateCredentialResponse error returned from framework")
+ callback.onError(convertToJetpackCreateException(error))
+ }
+ }
+
+ credentialManager!!.createCredential(
+ context,
+ convertCreateRequestToFrameworkClass(request, context),
+ cancellationSignal,
+ executor,
+ outcome
+ )
+ }
+
+ private fun convertCreateRequestToFrameworkClass(
+ request: CreateCredentialRequest,
+ context: Context
+ ): android.credentials.CreateCredentialRequest {
+ val createCredentialRequestBuilder: android.credentials.CreateCredentialRequest.Builder =
+ android.credentials.CreateCredentialRequest
+ .Builder(request.type,
+ FrameworkImplHelper.getFinalCreateCredentialData(request, context),
+ request.candidateQueryData)
+ .setIsSystemProviderRequired(request.isSystemProviderRequired)
+ // TODO("change to taking value from the request when ready")
+ .setAlwaysSendAppInfoToProvider(true)
+ setOriginForCreateRequest(request, createCredentialRequestBuilder)
+ return createCredentialRequestBuilder.build()
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun setOriginForCreateRequest(
+ request: CreateCredentialRequest,
+ builder: android.credentials.CreateCredentialRequest.Builder
+ ) {
+ if (request.origin != null) {
+ builder.setOrigin(request.origin)
+ }
+ }
+
+ private fun convertGetRequestToFrameworkClass(request: GetCredentialRequest):
+ android.credentials.GetCredentialRequest {
+ val builder = android.credentials.GetCredentialRequest.Builder(
+ GetCredentialRequest.toRequestDataBundle(request))
+ request.credentialOptions.forEach {
+ // TODO(b/278308121): clean up the temporary bundle value injection after the Beta 2
+ // release.
+ if (request.preferImmediatelyAvailableCredentials &&
+ it is GetPublicKeyCredentialOption) {
+ it.requestData.putBoolean(
+ "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS",
+ true,
+ )
+ }
+
+ 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()
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun setOriginForGetRequest(
+ request: GetCredentialRequest,
+ builder: android.credentials.GetCredentialRequest.Builder
+ ) {
+ if (request.origin != null) {
+ builder.setOrigin(request.origin)
+ }
+ }
+
+ private fun createFrameworkClearCredentialRequest():
+ android.credentials.ClearCredentialStateRequest {
+ return android.credentials.ClearCredentialStateRequest(Bundle())
+ }
+
+ internal fun convertToJetpackGetException(error: android.credentials.GetCredentialException):
+ GetCredentialException {
+ return when (error.type) {
+ android.credentials.GetCredentialException.TYPE_NO_CREDENTIAL ->
+ NoCredentialException(error.message)
+
+ android.credentials.GetCredentialException.TYPE_USER_CANCELED ->
+ GetCredentialCancellationException(error.message)
+
+ android.credentials.GetCredentialException.TYPE_INTERRUPTED ->
+ GetCredentialInterruptedException(error.message)
+
+ else -> GetCredentialUnknownException(error.message)
+ }
+ }
+
+ internal fun convertToJetpackCreateException(
+ error: android.credentials.CreateCredentialException
+ ): CreateCredentialException {
+ return when (error.type) {
+ android.credentials.CreateCredentialException.TYPE_NO_CREATE_OPTIONS ->
+ CreateCredentialNoCreateOptionException(error.message)
+
+ android.credentials.CreateCredentialException.TYPE_USER_CANCELED ->
+ CreateCredentialCancellationException(error.message)
+
+ android.credentials.CreateCredentialException.TYPE_INTERRUPTED ->
+ CreateCredentialInterruptedException(error.message)
+
+ else -> CreateCredentialUnknownException(error.message)
+ }
+ }
+
+ internal fun convertGetResponseToJetpackClass(
+ response: android.credentials.GetCredentialResponse
+ ): GetCredentialResponse {
+ val credential = response.credential
+ return GetCredentialResponse(
+ Credential.createFrom(
+ credential.type, credential.data
+ )
+ )
+ }
+
+ internal fun convertPrepareGetResponseToJetpackClass(
+ response: android.credentials.PrepareGetCredentialResponse
+ ): PrepareGetCredentialResponse {
+ return PrepareGetCredentialResponse(
+ response,
+ PrepareGetCredentialResponse.PendingGetCredentialHandle(
+ response.pendingGetCredentialHandle,
+ )
+ )
+ }
+
+ override fun isAvailableOnDevice(): Boolean {
+ // TODO("b/276492529 Base it on API level check")
+ return true
+ }
+
+ override fun onClearCredential(
+ request: ClearCredentialStateRequest,
+ cancellationSignal: CancellationSignal?,
+ executor: Executor,
+ callback: CredentialManagerCallback<Void?, ClearCredentialException>
+ ) {
+ Log.i(TAG, "In CredentialProviderFrameworkImpl onClearCredential")
+
+ if (isCredmanDisabled { ->
+ callback.onError(
+ ClearCredentialUnsupportedException(
+ "Your device doesn't support credential manager"
+ )
+ )
+ }) return
+
+ val outcome = object : OutcomeReceiver<Void,
+ android.credentials.ClearCredentialStateException> {
+ override fun onResult(response: Void) {
+ Log.i(TAG, "Clear result returned from framework: ")
+ callback.onResult(response)
+ }
+
+ override fun onError(error: android.credentials.ClearCredentialStateException) {
+ Log.i(TAG, "ClearCredentialStateException error returned from framework")
+ // TODO("Covert to the appropriate exception")
+ callback.onError(ClearCredentialUnknownException())
+ }
+ }
+
+ credentialManager!!.clearCredentialState(
+ createFrameworkClearCredentialRequest(),
+ cancellationSignal,
+ executor,
+ outcome
+ )
+ }
+
+ /** @hide */
+ companion object {
+ private const val TAG = "CredManProvService"
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CustomCredential.kt b/credentials/credentials/src/main/java/androidx/credentials/CustomCredential.kt
index 7550543..a229a32 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CustomCredential.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CustomCredential.kt
@@ -30,15 +30,17 @@
* Note: The Bundle keys for [data] should not be in the form of `androidx.credentials.*` as they
* are reserved for internal use by this androidx library.
*
- * @property type the credential type determined by the credential-type-specific subclass for custom
+ * @param type the credential type determined by the credential-type-specific subclass for custom
* use cases
- * @property data the credential data in the [Bundle] format for custom use cases
+ * @param data the credential data in the [Bundle] format for custom use cases (note: bundle keys in
+ * the form of `androidx.credentials.*` and `android.credentials.*` are reserved for internal
+ * library usage)
* @throws IllegalArgumentException If [type] is empty
* @throws NullPointerException If [data] or [type] is null
*/
open class CustomCredential(
- final override val type: String,
- final override val data: Bundle
+ type: String,
+ data: Bundle
) : Credential(type, data) {
init {
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
index 225880c..f154951 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
@@ -16,6 +16,10 @@
package androidx.credentials
+import android.content.ComponentName
+import android.os.Bundle
+import androidx.credentials.internal.FrameworkClassParsingException
+
/**
* Encapsulates a request to get a user credential.
*
@@ -28,12 +32,41 @@
* @property origin the origin of a different application if the request is being made on behalf of
* that application. For API level >=34, setting a non-null value for this parameter, will throw
* a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present.
+ * @property preferIdentityDocUi the value which signals if the UI should be tailored to display an
+ * identity document like driver license etc.
+ * @property preferUiBrandingComponentName a service [ComponentName] from which the Credential
+ * Selector UI will pull its label and icon to render top level branding. Your app must have the
+ * permission android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS to specify this, or
+ * it would not take effect. Notice that this bit may not take effect for Android API level
+ * 33 and below, depending on the pre-34 provider(s) you have chosen.
+ * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * immediately when there is no available credentials instead of falling back to discovering remote
+ * options, and false (default) otherwise
+ * @param credentialOptions the list of [CredentialOption] from which the user can choose
+ * one to authenticate to the app
+ * @param origin the origin of a different application if the request is being made on behalf of
+ * that application (Note: for API level >=34, setting a non-null value for this parameter, will
+ * throw a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present)
+ * @param preferIdentityDocUi the value which signals if the UI should be tailored to display an
+ * identity document like driver license etc
+ * @param preferUiBrandingComponentName a service [ComponentName] from which the Credential
+ * Selector UI will pull its label and icon to render top level branding (Note: your app must have
+ * the permission android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS to specify this, or
+ * it would not take effect; also this bit may not take effect for Android API level 33 and below,
+ * depending on the pre-34 provider(s) you have chosen
+ * @param preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * immediately when there is no available credentials instead of falling back to discovering remote
+ * options, and false (default) otherwise
* @throws IllegalArgumentException If [credentialOptions] is empty
*/
class GetCredentialRequest
@JvmOverloads constructor(
val credentialOptions: List<CredentialOption>,
val origin: String? = null,
+ val preferIdentityDocUi: Boolean = false,
+ val preferUiBrandingComponentName: ComponentName? = null,
+ @get:JvmName("preferImmediatelyAvailableCredentials")
+ val preferImmediatelyAvailableCredentials: Boolean = false,
) {
init {
@@ -44,6 +77,9 @@
class Builder {
private var credentialOptions: MutableList<CredentialOption> = mutableListOf()
private var origin: String? = null
+ private var preferIdentityDocUi: Boolean = false
+ private var preferImmediatelyAvailableCredentials: Boolean = false
+ private var preferUiBrandingComponentName: ComponentName? = null
/** Adds a specific type of [CredentialOption]. */
fun addCredentialOption(credentialOption: CredentialOption): Builder {
@@ -57,22 +93,116 @@
return this
}
- /** Sets the [origin] of a different application if the request is being made on behalf of
+ /**
+ * Sets the [origin] of a different application if the request is being made on behalf of
* that application. For API level >=34, setting a non-null value for this parameter, will
* throw a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not
- * present. */
+ * present.
+ */
fun setOrigin(origin: String): Builder {
this.origin = origin
return this
}
/**
+ * Sets whether you prefer the operation to return immediately when there is no available
+ * credentials instead of falling back to discovering remote options. The default value
+ * is false.
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ fun setPreferImmediatelyAvailableCredentials(
+ preferImmediatelyAvailableCredentials: Boolean
+ ): Builder {
+ this.preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
+ return this
+ }
+
+ /**
+ * Sets service [ComponentName] from which the Credential Selector UI will pull its label
+ * and icon to render top level branding. Your app must have the
+ * permission android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS to specify this,
+ * or it would not take effect. Notice that this bit may not take effect for Android API
+ * level 33 and below, depending on the pre-34 provider(s) you have chosen.
+ */
+ fun setPreferUiBrandingComponentName(component: ComponentName?): Builder {
+ this.preferUiBrandingComponentName = component
+ return this
+ }
+
+ /**
+ * Sets the [Boolean] preferIdentityDocUi to true if the requester wants to prefer using a
+ * UI suited for Identity Documents like mDocs, Driving License etc.
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ fun setPreferIdentityDocUi(preferIdentityDocUi: Boolean): Builder {
+ this.preferIdentityDocUi = preferIdentityDocUi
+ return this
+ }
+
+ /**
* Builds a [GetCredentialRequest].
*
* @throws IllegalArgumentException If [credentialOptions] is empty
*/
fun build(): GetCredentialRequest {
- return GetCredentialRequest(credentialOptions.toList(), origin)
+ return GetCredentialRequest(
+ credentialOptions.toList(),
+ origin,
+ preferIdentityDocUi,
+ preferUiBrandingComponentName,
+ preferImmediatelyAvailableCredentials
+ )
+ }
+ }
+
+ /** @hide */
+ companion object {
+ internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
+ "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
+ internal const val BUNDLE_KEY_PREFER_IDENTITY_DOC_UI =
+ "androidx.credentials.BUNDLE_KEY_PREFER_IDENTITY_DOC_UI"
+ internal const val BUNDLE_KEY_PREFER_UI_BRANDING_COMPONENT_NAME =
+ "androidx.credentials.BUNDLE_KEY_PREFER_UI_BRANDING_COMPONENT_NAME"
+
+ /** @hide */
+ @JvmStatic
+ fun toRequestDataBundle(
+ request: GetCredentialRequest
+ ): Bundle {
+ val bundle = Bundle()
+ bundle.putBoolean(BUNDLE_KEY_PREFER_IDENTITY_DOC_UI, request.preferIdentityDocUi)
+ bundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+ request.preferImmediatelyAvailableCredentials)
+ bundle.putParcelable(
+ BUNDLE_KEY_PREFER_UI_BRANDING_COMPONENT_NAME, request.preferUiBrandingComponentName)
+ return bundle
+ }
+
+ /** @hide */
+ @JvmStatic
+ fun createFrom(
+ credentialOptions: List<CredentialOption>,
+ origin: String?,
+ data: Bundle
+ ): GetCredentialRequest {
+ try {
+ val preferIdentityDocUi = data.getBoolean(BUNDLE_KEY_PREFER_IDENTITY_DOC_UI)
+ val preferImmediatelyAvailableCredentials = data.getBoolean(
+ BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
+ @Suppress("DEPRECATION")
+ val preferUiBrandingComponentName = data.getParcelable<ComponentName>(
+ BUNDLE_KEY_PREFER_UI_BRANDING_COMPONENT_NAME)
+ var getCredentialBuilder = Builder().setCredentialOptions(credentialOptions)
+ .setPreferIdentityDocUi(preferIdentityDocUi)
+ .setPreferUiBrandingComponentName(preferUiBrandingComponentName)
+ .setPreferImmediatelyAvailableCredentials(preferImmediatelyAvailableCredentials)
+ if (origin != null) {
+ getCredentialBuilder.setOrigin(origin)
+ }
+ return getCredentialBuilder.build()
+ } catch (e: Exception) {
+ throw FrameworkClassParsingException()
+ }
}
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetCustomCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/GetCustomCredentialOption.kt
index 775e331..5ced43a 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetCustomCredentialOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetCustomCredentialOption.kt
@@ -16,6 +16,7 @@
package androidx.credentials
+import android.content.ComponentName
import android.os.Bundle
/**
@@ -29,37 +30,44 @@
* Note: The Bundle keys for [requestData] and [candidateQueryData] should not be in the form of
* `androidx.credentials.*` as they are reserved for internal use by this androidx library.
*
- * @property type the credential type determined by the credential-type-specific subclass
+ * @param type the credential type determined by the credential-type-specific subclass
* generated for custom use cases
- * @property requestData the request data in the [Bundle] format, generated for custom use cases
- * @property candidateQueryData the partial request data in the [Bundle] format that will be sent to
+ * @param requestData the request data in the [Bundle] format, generated for custom use cases
+ * (note: bundle keys in the form of `androidx.credentials.*` and `android.credentials.*` are
+ * reserved for internal library usage)
+ * @param candidateQueryData the partial request data in the [Bundle] format that will be sent to
* the provider during the initial candidate query stage, which should not contain sensitive user
- * information
- * @property isSystemProviderRequired true if must only be fulfilled by a system provider and false
+ * information (note: bundle keys in the form of `androidx.credentials.*` and
+ * `android.credentials.*` are reserved for internal library usage)
+ * @param isSystemProviderRequired true if must only be fulfilled by a system provider and false
* otherwise
- * @property isAutoSelectAllowed defines if a credential entry will be automatically chosen if it is
+ * @param isAutoSelectAllowed defines if a credential entry will be automatically chosen if it is
* the only one available option, false by default
+ * @param allowedProviders a set of provider service [ComponentName] allowed to receive this
+ * option (Note: a [SecurityException] will be thrown if it is set as non-empty but your app does
+ * not have android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS; for API level < 34,
+ * this property will not take effect and you should control the allowed provider via
+ * [library dependencies](https://developer.android.com/training/sign-in/passkeys#add-dependencies))
* @throws IllegalArgumentException If [type] is empty
* @throws NullPointerException If [requestData] or [type] is null
*/
open class GetCustomCredentialOption @JvmOverloads constructor(
- final override val type: String,
- final override val requestData: Bundle,
- final override val candidateQueryData: Bundle,
- final override val isSystemProviderRequired: Boolean,
- final override val isAutoSelectAllowed: Boolean = false,
+ type: String,
+ requestData: Bundle,
+ candidateQueryData: Bundle,
+ isSystemProviderRequired: Boolean,
+ isAutoSelectAllowed: Boolean = false,
+ allowedProviders: Set<ComponentName> = emptySet(),
) : CredentialOption(
type = type,
requestData = requestData,
candidateQueryData = candidateQueryData,
isSystemProviderRequired = isSystemProviderRequired,
- isAutoSelectAllowed = isAutoSelectAllowed
+ isAutoSelectAllowed = isAutoSelectAllowed,
+ allowedProviders = allowedProviders,
) {
init {
- if (!requestData.containsKey(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED)) {
- requestData.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, isAutoSelectAllowed)
- }
require(type.isNotEmpty()) { "type should not be empty" }
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetPasswordOption.kt b/credentials/credentials/src/main/java/androidx/credentials/GetPasswordOption.kt
index a65188e..0996838 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetPasswordOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetPasswordOption.kt
@@ -16,29 +16,60 @@
package androidx.credentials
+import android.content.ComponentName
import android.os.Bundle
-/** A request to retrieve the user's saved application password from their password provider.
+/**
+ * A request to retrieve the user's saved application password from their password provider.
*
- * @property isAutoSelectAllowed false by default, allows auto selecting a password if there is
+ * @property allowedUserIds a optional set of user ids with which the credentials associated are
+ * requested; leave as empty if you want to request all the available user credentials
+ * @param allowedUserIds a optional set of user ids with which the credentials associated are
+ * requested; leave as empty if you want to request all the available user credentials
+ * @param isAutoSelectAllowed false by default, allows auto selecting a password if there is
* only one available
+ * @param allowedProviders a set of provider service [ComponentName] allowed to receive this
+ * option (Note: a [SecurityException] will be thrown if it is set as non-empty but your app does
+ * not have android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS; for API level < 34,
+ * this property will not take effect and you should control the allowed provider via
+ * [library dependencies](https://developer.android.com/training/sign-in/passkeys#add-dependencies))
*/
class GetPasswordOption @JvmOverloads constructor(
- override val isAutoSelectAllowed: Boolean = false
+ val allowedUserIds: Set<String> = emptySet(),
+ isAutoSelectAllowed: Boolean = false,
+ allowedProviders: Set<ComponentName> = emptySet(),
) : CredentialOption(
type = PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
- requestData = Bundle(),
- candidateQueryData = Bundle(),
+ requestData = toBundle(allowedUserIds),
+ candidateQueryData = toBundle(allowedUserIds),
isSystemProviderRequired = false,
isAutoSelectAllowed = isAutoSelectAllowed,
+ allowedProviders,
) {
/** @hide */
companion object {
- @Suppress("UNUSED_PARAMETER")
+ internal const val BUNDLE_KEY_ALLOWED_USER_IDS =
+ "androidx.credentials.BUNDLE_KEY_ALLOWED_USER_IDS"
+
@JvmStatic
- internal fun createFrom(data: Bundle): GetPasswordOption {
- return GetPasswordOption()
+ internal fun createFrom(
+ data: Bundle,
+ allowedProviders: Set<ComponentName>,
+ ): GetPasswordOption {
+ val allowUserIdList = data.getStringArrayList(BUNDLE_KEY_ALLOWED_USER_IDS)
+ return GetPasswordOption(
+ allowUserIdList?.toSet() ?: emptySet(),
+ data.getBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, false),
+ allowedProviders
+ )
+ }
+
+ @JvmStatic
+ internal fun toBundle(allowUserIds: Set<String>): Bundle {
+ val bundle = Bundle()
+ bundle.putStringArrayList(BUNDLE_KEY_ALLOWED_USER_IDS, ArrayList(allowUserIds))
+ return bundle
}
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt
index cdd6e76..1ab47ac 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt
@@ -16,6 +16,7 @@
package androidx.credentials
+import android.content.ComponentName
import android.os.Bundle
import androidx.credentials.internal.FrameworkClassParsingException
@@ -24,27 +25,33 @@
*
* @property requestJson the request in JSON format in the standard webauthn web json
* shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
- * @property clientDataHash a hash that is used to verify the relying party identity, set only if
- * you have set the [GetCredentialRequest.origin]
- * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
- * immediately when there is no available credential instead of falling back to discovering remote
- * credentials, and false (default) otherwise
+ * @property clientDataHash a clientDataHash value to sign over in place of assembling and hashing
+ * clientDataJSON during the signature request; meaningful only if you have set the
+ * [GetCredentialRequest.origin]
+ * @param requestJson the request in JSON format in the standard webauthn web json
+ * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
+ * @param clientDataHash a clientDataHash value to sign over in place of assembling and hashing
+ * clientDataJSON during the signature request; set only if you have set the
+ * [GetCredentialRequest.origin]
+ * @param allowedProviders a set of provider service [ComponentName] allowed to receive this
+ * option (Note: a [SecurityException] will be thrown if it is set as non-empty but your app does
+ * not have android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS; for API level < 34,
+ * this property will not take effect and you should control the allowed provider via
+ * [library dependencies](https://developer.android.com/training/sign-in/passkeys#add-dependencies))
* @throws NullPointerException If [requestJson] is null
* @throws IllegalArgumentException If [requestJson] is empty
*/
class GetPublicKeyCredentialOption @JvmOverloads constructor(
val requestJson: String,
- val clientDataHash: String? = null,
- @get:JvmName("preferImmediatelyAvailableCredentials")
- val preferImmediatelyAvailableCredentials: Boolean = false,
+ val clientDataHash: ByteArray? = null,
+ allowedProviders: Set<ComponentName> = emptySet(),
) : CredentialOption(
type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
- requestData = toRequestDataBundle(requestJson, clientDataHash,
- preferImmediatelyAvailableCredentials),
- candidateQueryData = toRequestDataBundle(requestJson, clientDataHash,
- preferImmediatelyAvailableCredentials),
+ requestData = toRequestDataBundle(requestJson, clientDataHash),
+ candidateQueryData = toRequestDataBundle(requestJson, clientDataHash),
isSystemProviderRequired = false,
isAutoSelectAllowed = true,
+ allowedProviders,
) {
init {
require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
@@ -52,8 +59,6 @@
/** @hide */
companion object {
- internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
- "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
internal const val BUNDLE_KEY_CLIENT_DATA_HASH =
"androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
@@ -63,8 +68,7 @@
@JvmStatic
internal fun toRequestDataBundle(
requestJson: String,
- clientDataHash: String?,
- preferImmediatelyAvailableCredentials: Boolean
+ clientDataHash: ByteArray?,
): Bundle {
val bundle = Bundle()
bundle.putString(
@@ -72,24 +76,25 @@
BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION
)
bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
- bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
- bundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
- preferImmediatelyAvailableCredentials)
+ bundle.putByteArray(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
return bundle
}
@Suppress("deprecation") // bundle.get() used for boolean value to prevent default
// boolean value from being returned.
@JvmStatic
- internal fun createFrom(data: Bundle): GetPublicKeyCredentialOption {
+ internal fun createFrom(
+ data: Bundle,
+ allowedProviders: Set<ComponentName>,
+ ): GetPublicKeyCredentialOption {
try {
val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
- val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
- val preferImmediatelyAvailableCredentials =
- data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
- return GetPublicKeyCredentialOption(requestJson!!,
+ val clientDataHash = data.getByteArray(BUNDLE_KEY_CLIENT_DATA_HASH)
+ return GetPublicKeyCredentialOption(
+ requestJson!!,
clientDataHash,
- (preferImmediatelyAvailableCredentials!!) as Boolean)
+ allowedProviders
+ )
} catch (e: Exception) {
throw FrameworkClassParsingException()
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/PasswordCredential.kt b/credentials/credentials/src/main/java/androidx/credentials/PasswordCredential.kt
index 6df28d8..97767e0 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/PasswordCredential.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/PasswordCredential.kt
@@ -17,7 +17,6 @@
package androidx.credentials
import android.os.Bundle
-import androidx.annotation.VisibleForTesting
import androidx.credentials.internal.FrameworkClassParsingException
/**
@@ -38,17 +37,13 @@
require(password.isNotEmpty()) { "password should not be empty" }
}
- /** @hide */
+ /** Companion constants / helpers for [PasswordCredential]. */
companion object {
- // TODO: this type is officially defined in the framework. This definition should be
- // removed when the framework type is available in jetpack.
- /** @hide */
+ /** The type value for password related operations. */
const val TYPE_PASSWORD_CREDENTIAL: String = "android.credentials.TYPE_PASSWORD_CREDENTIAL"
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- const val BUNDLE_KEY_ID = "androidx.credentials.BUNDLE_KEY_ID"
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- const val BUNDLE_KEY_PASSWORD = "androidx.credentials.BUNDLE_KEY_PASSWORD"
+ internal const val BUNDLE_KEY_ID = "androidx.credentials.BUNDLE_KEY_ID"
+ internal const val BUNDLE_KEY_PASSWORD = "androidx.credentials.BUNDLE_KEY_PASSWORD"
@JvmStatic
internal fun toBundle(id: String, password: String): Bundle {
diff --git a/credentials/credentials/src/main/java/androidx/credentials/PrepareGetCredentialResponse.kt b/credentials/credentials/src/main/java/androidx/credentials/PrepareGetCredentialResponse.kt
new file mode 100644
index 0000000..d19cbb9
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/PrepareGetCredentialResponse.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.credentials
+
+import android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS
+import android.credentials.PrepareGetCredentialResponse
+import androidx.annotation.OptIn
+import androidx.annotation.RequiresApi
+import androidx.annotation.RequiresPermission
+import androidx.annotation.RestrictTo
+import androidx.core.os.BuildCompat
+
+/**
+ * A response object that indicates the get-credential prefetch work is complete and provides
+ * metadata about it. It can then be used to issue the full credential retrieval flow via the
+ * [CredentialManager.getCredential] (Kotlin) / [CredentialManager.getCredentialAsync] (Java)
+ * method to perform the remaining flows such as consent
+ * collection and credential selection, to officially retrieve a credential.
+ *
+ * For now this API requires Android U (level 34). However, it is designed with backward
+ * compatibility in mind and can potentially be made accessible <34 if any provider decides to
+ * support that.
+ *
+ * @property frameworkResponse the corresponding framework response, guaranteed to be nonnull
+ * at API level >= 34
+ * @property pendingGetCredentialHandle a handle that represents this pending get-credential
+ * operation; pass this handle to [CredentialManager.getCredential] (Kotlin) /
+ * [CredentialManager.getCredentialAsync] (Java) to perform the remaining flows to officially
+ * retrieve a credential.
+ * @throws NullPointerException If [frameworkResponse] is null at API level >= 34.
+ */
+@RequiresApi(34)
+@OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
+class PrepareGetCredentialResponse internal constructor(
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ val frameworkResponse: PrepareGetCredentialResponse?,
+ val pendingGetCredentialHandle: PendingGetCredentialHandle,
+) {
+ init {
+ if (BuildCompat.isAtLeastU()) {
+ frameworkResponse!!
+ }
+ }
+
+ /**
+ * Returns true if the user has any candidate credentials for the given {@code credentialType},
+ * and false otherwise.
+ *
+ * Note: this API will always return false at API level < 34.
+ */
+ @Suppress("UNUSED_PARAMETER")
+ @RequiresPermission(CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS)
+ fun hasCredentialResults(credentialType: String): Boolean {
+ return frameworkResponse?.hasCredentialResults(credentialType) ?: false
+ }
+
+ /**
+ * Returns true if the user has any candidate authentication actions (locked credential
+ * supplier), and false otherwise.
+ *
+ * Note: this API will always return false at API level < 34.
+ */
+ @RequiresPermission(CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS)
+ fun hasAuthenticationResults(): Boolean {
+ return frameworkResponse?.hasAuthenticationResults() ?: false
+ }
+
+ /**
+ * Returns true if the user has any candidate remote credential results, and false otherwise.
+ *
+ * Note: this API will always return false at API level < 34.
+ */
+ @RequiresPermission(CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS)
+ fun hasRemoteResults(): Boolean {
+ return frameworkResponse?.hasRemoteResults() ?: false
+ }
+
+ /**
+ * A handle that represents a pending get-credential operation. Pass this handle to
+ * [CredentialManager.getCredential] or [CredentialManager.getCredentialAsync] to perform the
+ * remaining flows to officially retrieve a credential.
+ *
+ * @property frameworkHandle the framework handle representing this pending operation. Must not
+ * be null at API level >= 34.
+ * @throws NullPointerException If [frameworkHandle] is null at API level >= 34.
+ */
+ @OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
+ @RequiresApi(34)
+ class PendingGetCredentialHandle(
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ val frameworkHandle:
+ PrepareGetCredentialResponse.PendingGetCredentialHandle?
+ ) {
+ init {
+ if (BuildCompat.isAtLeastU()) {
+ frameworkHandle!!
+ }
+ }
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/PublicKeyCredential.kt b/credentials/credentials/src/main/java/androidx/credentials/PublicKeyCredential.kt
index d0640fe..c2992eb 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/PublicKeyCredential.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/PublicKeyCredential.kt
@@ -17,7 +17,6 @@
package androidx.credentials
import android.os.Bundle
-import androidx.annotation.VisibleForTesting
import androidx.credentials.internal.FrameworkClassParsingException
/**
@@ -41,18 +40,14 @@
"authentication response JSON must not be empty" }
}
- /** @hide */
+ /** Companion constants / helpers for [PublicKeyCredential]. */
companion object {
- /**
- * The type value for public key credential related operations.
- *
- * @hide
- */
+ /** The type value for public key credential related operations. */
const val TYPE_PUBLIC_KEY_CREDENTIAL: String =
"androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"
+
/** The Bundle key value for the public key credential subtype (privileged or regular). */
internal const val BUNDLE_KEY_SUBTYPE = "androidx.credentials.BUNDLE_KEY_SUBTYPE"
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal const val BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON =
"androidx.credentials.BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON"
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 ab39cd4..c533279 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
@@ -38,14 +38,14 @@
@RequiresApi(23)
fun getFinalCreateCredentialData(
request: CreateCredentialRequest,
- activity: Context,
+ context: Context,
): Bundle {
val createCredentialData = request.credentialData
val displayInfoBundle = request.displayInfo.toBundle()
displayInfoBundle.putParcelable(
CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_CREDENTIAL_TYPE_ICON,
Icon.createWithResource(
- activity,
+ context,
when (request) {
is CreatePasswordRequest -> R.drawable.ic_password
is CreatePublicKeyCredentialRequest -> R.drawable.ic_passkey
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt
new file mode 100644
index 0000000..39106a6
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt
@@ -0,0 +1,186 @@
+/*
+ * 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.credentials.provider
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.app.slice.Slice
+import android.app.slice.SliceSpec
+import android.net.Uri
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import java.util.Collections
+
+/**
+ * An actionable entry that is returned as part of the
+ * [android.service.credentials.BeginGetCredentialResponse], and then shown on the user selector.
+ * An action entry is expected to navigate the user to the credential provider's activity, and
+ * ultimately result in a [androidx.credentials.GetCredentialResponse] through that activity.
+ *
+ * When selected, the associated [PendingIntent] is invoked to launch a provider controlled
+ * activity. The activity invoked due to this pending intent will contain the
+ * [android.service.credentials.BeginGetCredentialRequest] as part of the intent extras. Providers
+ * must use [PendingIntentHandler.retrieveBeginGetCredentialRequest] to get the request.
+ *
+ * When the user is done interacting with the activity and the provider has a credential to return,
+ * provider must call [android.app.Activity.setResult] with the result code as
+ * [android.app.Activity.RESULT_OK], and the [android.content.Intent] data that has been prepared
+ * by using [PendingIntentHandler.setGetCredentialResponse], before ending the activity.
+ * If the provider does not have a credential to return, provider must call
+ * [android.app.Activity.setResult] with the result code as [android.app.Activity.RESULT_CANCELED].
+ *
+ * Examples of [Action] entries include an entry that is titled 'Add a new Password', and navigates
+ * to the 'add password' page of the credential provider app, or an entry that is titled
+ * 'Manage Credentials' and navigates to a particular page that lists all credentials, where the
+ * user may end up selecting a credential that the provider can then return.
+ *
+ * @property title the title of the entry
+ * @property pendingIntent the [PendingIntent] that will be invoked when the user selects this entry
+ * @property subtitle the optional subtitle that is displayed on the entry
+ *
+ * @see android.service.credentials.BeginGetCredentialResponse for usage.
+ *
+ * @throws IllegalArgumentException If [title] is empty
+ * @throws NullPointerException If [title] or [pendingIntent] is null
+ */
+class Action constructor(
+ val title: CharSequence,
+ val pendingIntent: PendingIntent,
+ val subtitle: CharSequence? = null,
+) {
+
+ init {
+ require(title.isNotEmpty()) { "title must not be empty" }
+ }
+
+ /**
+ * A builder for [Action]
+ *
+ * @param title the title of this action entry
+ * @param pendingIntent the [PendingIntent] that will be fired when the user selects
+ * this action entry
+ */
+ class Builder constructor(
+ private val title: CharSequence,
+ private val pendingIntent: PendingIntent
+ ) {
+ private var subtitle: CharSequence? = null
+
+ /** Sets a sub title to be shown on the UI with this entry */
+ fun setSubtitle(subtitle: CharSequence?): Builder {
+ this.subtitle = subtitle
+ return this
+ }
+
+ /**
+ * Builds an instance of [Action]
+ *
+ * @throws IllegalArgumentException If [title] is empty
+ */
+ fun build(): Action {
+ return Action(title, pendingIntent, subtitle)
+ }
+ }
+
+ /** @hide **/
+ @Suppress("AcronymName")
+ companion object {
+ private const val TAG = "Action"
+ private const val SLICE_SPEC_REVISION = 0
+ private const val SLICE_SPEC_TYPE = "Action"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_TITLE =
+ "androidx.credentials.provider.action.HINT_ACTION_TITLE"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_SUBTITLE =
+ "androidx.credentials.provider.action.HINT_ACTION_SUBTEXT"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_PENDING_INTENT =
+ "androidx.credentials.provider.action.SLICE_HINT_PENDING_INTENT"
+
+ /**
+ * Converts to slice
+ * @hide
+ */
+ @JvmStatic
+ @RequiresApi(28)
+ fun toSlice(
+ action: Action
+ ): Slice {
+ val title = action.title
+ val subtitle = action.subtitle
+ val pendingIntent = action.pendingIntent
+ val sliceBuilder = Slice.Builder(
+ Uri.EMPTY, SliceSpec(
+ SLICE_SPEC_TYPE, SLICE_SPEC_REVISION
+ )
+ )
+ .addText(
+ title, /*subType=*/null,
+ listOf(SLICE_HINT_TITLE)
+ )
+ .addText(
+ subtitle, /*subType=*/null,
+ listOf(SLICE_HINT_SUBTITLE)
+ )
+ sliceBuilder.addAction(
+ pendingIntent,
+ Slice.Builder(sliceBuilder)
+ .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
+ .build(),
+ /*subType=*/null
+ )
+ return sliceBuilder.build()
+ }
+
+ /**
+ * Returns an instance of [Action] derived from a [Slice] object.
+ *
+ * @param slice the [Slice] object constructed through [toSlice]
+ *
+ * @hide
+ */
+ @RequiresApi(28)
+ @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+ @JvmStatic
+ fun fromSlice(slice: Slice): Action? {
+ var title: CharSequence = ""
+ var subtitle: CharSequence? = null
+ var pendingIntent: PendingIntent? = null
+
+ slice.items.forEach {
+ if (it.hasHint(SLICE_HINT_TITLE)) {
+ title = it.text
+ } else if (it.hasHint(SLICE_HINT_SUBTITLE)) {
+ subtitle = it.text
+ } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
+ pendingIntent = it.action
+ }
+ }
+
+ return try {
+ Action(title, pendingIntent!!, subtitle)
+ } catch (e: Exception) {
+ Log.i(TAG, "fromSlice failed with: " + e.message)
+ null
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt
new file mode 100644
index 0000000..e468930
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.credentials.provider
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.app.slice.Slice
+import android.app.slice.SliceSpec
+import android.net.Uri
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import java.util.Collections
+
+/**
+ * An entry on the selector, denoting that the provider service is locked and authentication
+ * is needed to proceed.
+ *
+ * Providers should set this entry when the provider app is locked, and no credentials can
+ * be returned.
+ * Providers must set the [PendingIntent] that leads to their unlock activity. When the user
+ * selects this entry, the corresponding [PendingIntent] is fired and the unlock activity is
+ * invoked. Once the provider authentication flow is complete, providers must set
+ * the [android.service.credentials.BeginGetCredentialResponse] containing the unlocked credential
+ * entries, through the [PendingIntentHandler.setBeginGetCredentialResponse] method, before
+ * finishing the activity.
+ * If providers fail to set the [android.service.credentials.BeginGetCredentialResponse], the
+ * system will assume that there are no credentials available and the this entry will be removed
+ * from the selector.
+ *
+ * @property pendingIntent the [PendingIntent] to be invoked if the user selects
+ * this authentication entry on the UI
+ * @property title the title to be shown with this entry on the account selector UI
+ *
+ * @see android.service.credentials.BeginGetCredentialResponse
+ * for usage details.
+ *
+ * @throws NullPointerException If the [pendingIntent] is null
+ */
+class AuthenticationAction constructor(
+ val title: CharSequence,
+ val pendingIntent: PendingIntent,
+) {
+ /** @hide **/
+ @Suppress("AcronymName")
+ companion object {
+ private const val TAG = "AuthenticationAction"
+ private const val SLICE_SPEC_REVISION = 0
+ private const val SLICE_SPEC_TYPE = "AuthenticationAction"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_TITLE =
+ "androidx.credentials.provider.authenticationAction.SLICE_HINT_TITLE"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_PENDING_INTENT =
+ "androidx.credentials.provider.authenticationAction.SLICE_HINT_PENDING_INTENT"
+
+ /** @hide **/
+ @RequiresApi(28)
+ @JvmStatic
+ fun toSlice(authenticationAction: AuthenticationAction): Slice {
+ val title = authenticationAction.title
+ val pendingIntent = authenticationAction.pendingIntent
+ val sliceBuilder = Slice.Builder(
+ Uri.EMPTY, SliceSpec(
+ SLICE_SPEC_TYPE,
+ SLICE_SPEC_REVISION
+ )
+ )
+ sliceBuilder
+ .addAction(
+ pendingIntent,
+ Slice.Builder(sliceBuilder)
+ .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
+ .build(),
+ /*subType=*/null
+ )
+ .addText(title, /*subType=*/null, listOf(SLICE_HINT_TITLE))
+ return sliceBuilder.build()
+ }
+
+ /**
+ * Returns an instance of [AuthenticationAction] derived from a [Slice] object.
+ *
+ * @param slice the [Slice] object that contains the information required for
+ * constructing an instance of this class.
+ *
+ * @hide
+ */
+ @RequiresApi(28)
+ @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+ @JvmStatic
+ fun fromSlice(slice: Slice): AuthenticationAction? {
+ var title: CharSequence? = null
+ var pendingIntent: PendingIntent? = null
+
+ slice.items.forEach {
+ if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
+ pendingIntent = it.action
+ } else if (it.hasHint(SLICE_HINT_TITLE)) {
+ title = it.text
+ }
+ }
+ return try {
+ AuthenticationAction(title!!, pendingIntent!!)
+ } catch (e: Exception) {
+ Log.i(TAG, "fromSlice failed with: " + e.message)
+ null
+ }
+ }
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCredentialRequest.kt
new file mode 100644
index 0000000..f77e1b5
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCredentialRequest.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.credentials.provider
+
+import android.os.Bundle
+import android.service.credentials.CallingAppInfo
+import androidx.annotation.DoNotInline
+import androidx.annotation.OptIn
+import androidx.annotation.RequiresApi
+import androidx.core.os.BuildCompat
+import androidx.credentials.provider.utils.BeginCreateCredentialUtil
+
+/**
+ * Abstract request class for beginning a create credential request.
+ *
+ * This class is to be extended by structured create credential requests
+ * such as [BeginCreatePasswordCredentialRequest].
+ */
+abstract class BeginCreateCredentialRequest constructor(
+ val type: String,
+ val candidateQueryData: Bundle,
+ val callingAppInfo: CallingAppInfo?
+) {
+ @RequiresApi(34)
+ private object Api34Impl {
+ private const val REQUEST_KEY = "androidx.credentials.provider.BeginCreateCredentialRequest"
+
+ @JvmStatic
+ @DoNotInline
+ fun writeToBundle(bundle: Bundle, request: BeginCreateCredentialRequest) {
+ bundle.putParcelable(
+ REQUEST_KEY,
+ BeginCreateCredentialUtil.convertToFrameworkRequest(request)
+ )
+ }
+
+ @JvmStatic
+ @DoNotInline
+ fun readFromBundle(bundle: Bundle): BeginCreateCredentialRequest? {
+ val frameworkRequest = bundle.getParcelable(
+ REQUEST_KEY,
+ android.service.credentials.BeginCreateCredentialRequest::class.java
+ )
+ if (frameworkRequest != null) {
+ return BeginCreateCredentialUtil.convertToJetpackRequest(frameworkRequest)
+ }
+ return null
+ }
+ }
+
+ companion object {
+ /**
+ * Helper method to convert the class to a parcelable [Bundle], in case the class
+ * instance needs to be sent across a process. Consumers of this method should use
+ * [readFromBundle] to reconstruct the class instance back from the bundle returned here.
+ */
+ @JvmStatic
+ @OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
+ fun writeToBundle(request: BeginCreateCredentialRequest): Bundle {
+ val bundle = Bundle()
+ if (BuildCompat.isAtLeastU()) {
+ Api34Impl.writeToBundle(bundle, request)
+ }
+ return bundle
+ }
+
+ /**
+ * Helper method to convert a [Bundle] retrieved through [writeToBundle], back
+ * to an instance of [BeginCreateCredentialRequest].
+ */
+ @JvmStatic
+ @OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
+ fun readFromBundle(bundle: Bundle): BeginCreateCredentialRequest? {
+ if (BuildCompat.isAtLeastU()) {
+ Api34Impl.readFromBundle(bundle)
+ }
+ return null
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCredentialResponse.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCredentialResponse.kt
new file mode 100644
index 0000000..1a586cf
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCredentialResponse.kt
@@ -0,0 +1,166 @@
+/*
+ * 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.credentials.provider
+
+import android.os.Bundle
+import androidx.annotation.DoNotInline
+import androidx.annotation.OptIn
+import androidx.annotation.RequiresApi
+import androidx.core.os.BuildCompat
+import androidx.credentials.provider.utils.BeginCreateCredentialUtil
+
+/**
+ * Response to [BeginCreateCredentialRequest].
+ *
+ * Credential providers must add a list of [CreateEntry], and an
+ * optional [RemoteEntry] to this response.
+ *
+ * Each [CreateEntry] is displayed to the user on the account selector,
+ * as an account option where the given credential can be stored.
+ * A [RemoteEntry] is an entry on the selector, through which user can choose
+ * to create the credential on a remote device.
+ *
+ * @throws IllegalArgumentException If [createEntries] is empty
+ */
+class BeginCreateCredentialResponse constructor(
+ val createEntries: List<CreateEntry> = listOf(),
+ val remoteEntry: RemoteEntry? = null
+) {
+
+ /** Builder for [BeginCreateCredentialResponse]. **/
+ class Builder {
+ private var createEntries: MutableList<CreateEntry> = mutableListOf()
+ private var remoteEntry: RemoteEntry? = null
+
+ /**
+ * Sets the list of create entries to be shown on the UI.
+ *
+ * @throws IllegalArgumentException If [createEntries] is empty.
+ * @throws NullPointerException If [createEntries] is null, or any of its elements
+ * are null.
+ */
+ fun setCreateEntries(createEntries: List<CreateEntry>): Builder {
+ this.createEntries = createEntries.toMutableList()
+ return this
+ }
+
+ /**
+ * Adds an entry to the list of create entries to be shown on the UI.
+ *
+ * @throws NullPointerException If [createEntry] is null.
+ */
+ fun addCreateEntry(createEntry: CreateEntry): Builder {
+ createEntries.add(createEntry)
+ return this
+ }
+
+ /**
+ * Sets a remote create entry to be shown on the UI. Provider must set this entry if they
+ * wish to create the credential on a different device.
+ *
+ * <p> When constructing the {@link CreateEntry] object, the pending intent must be
+ * set such that it leads to an activity that can provide UI to fulfill the request on
+ * a remote device. When user selects this [remoteEntry], the system will
+ * invoke the pending intent set on the [CreateEntry].
+ *
+ * <p> Once the remote credential flow is complete, the [android.app.Activity]
+ * result should be set to [android.app.Activity#RESULT_OK] and an extra with the
+ * [CredentialProviderService#EXTRA_CREATE_CREDENTIAL_RESPONSE] key should be populated
+ * with a [android.credentials.CreateCredentialResponse] object.
+ *
+ * <p> Note that as a provider service you will only be able to set a remote entry if :
+ * - Provider service possesses the
+ * [android.Manifest.permission.PROVIDE_REMOTE_CREDENTIALS] permission.
+ * - Provider service is configured as the provider that can provide remote entries.
+ *
+ * If the above conditions are not met, setting back [BeginCreateCredentialResponse]
+ * on the callback from [CredentialProviderService#onBeginCreateCredential]
+ * will throw a [SecurityException].
+ */
+ fun setRemoteEntry(remoteEntry: RemoteEntry?): Builder {
+ this.remoteEntry = remoteEntry
+ return this
+ }
+
+ /**
+ * Builds a new instance of [BeginCreateCredentialResponse].
+ *
+ * @throws IllegalArgumentException If [createEntries] is empty
+ */
+ fun build(): BeginCreateCredentialResponse {
+ return BeginCreateCredentialResponse(createEntries.toList(), remoteEntry)
+ }
+ }
+
+ @RequiresApi(34)
+ private object Api34Impl {
+ private const val REQUEST_KEY =
+ "androidx.credentials.provider.BeginCreateCredentialResponse"
+
+ @JvmStatic
+ @DoNotInline
+ fun writeToBundle(bundle: Bundle, response: BeginCreateCredentialResponse) {
+ bundle.putParcelable(
+ REQUEST_KEY,
+ BeginCreateCredentialUtil.convertToFrameworkResponse(response)
+ )
+ }
+
+ @JvmStatic
+ @DoNotInline
+ fun readFromBundle(bundle: Bundle): BeginCreateCredentialResponse? {
+ val frameworkResponse = bundle.getParcelable(
+ REQUEST_KEY,
+ android.service.credentials.BeginCreateCredentialResponse::class.java
+ )
+ if (frameworkResponse != null) {
+ return BeginCreateCredentialUtil.convertToJetpackResponse(frameworkResponse)
+ }
+ return null
+ }
+ }
+
+ companion object {
+ /**
+ * Helper method to convert the class to a parcelable [Bundle], in case the class
+ * instance needs to be sent across a process. Consumers of this method should use
+ * [readFromBundle] to reconstruct the class instance back from the bundle returned here.
+ */
+ @JvmStatic
+ @OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
+ fun writeToBundle(response: BeginCreateCredentialResponse): Bundle {
+ val bundle = Bundle()
+ if (BuildCompat.isAtLeastU()) {
+ Api34Impl.writeToBundle(bundle, response)
+ }
+ return bundle
+ }
+
+ /**
+ * Helper method to convert a [Bundle] retrieved through [writeToBundle], back
+ * to an instance of [BeginGetCredentialResponse].
+ */
+ @JvmStatic
+ @OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
+ fun readFromBundle(bundle: Bundle): BeginCreateCredentialResponse? {
+ if (BuildCompat.isAtLeastU()) {
+ Api34Impl.readFromBundle(bundle)
+ }
+ return null
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCustomCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCustomCredentialRequest.kt
new file mode 100644
index 0000000..07edf25
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCustomCredentialRequest.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.credentials.provider
+
+import android.os.Bundle
+import android.service.credentials.CallingAppInfo
+
+/**
+ * Base custom begin create request class for registering a credential.
+ *
+ * If you get a [BeginCreateCustomCredentialRequest] instead of a type-safe request class such as
+ * [BeginCreatePasswordCredentialRequest], [BeginCreatePublicKeyCredentialRequest], etc., then
+ * as a credential provider, you should check if you have any other library at interest that
+ * supports this custom [type] of credential request,
+ * and if so use its parsing utilities to resolve to a type-safe class within that library.
+ *
+ * Note: The Bundle keys for [candidateQueryData] should not be in the form
+ * of androidx.credentials.*` as they are reserved for internal use by this androidx library.
+ *
+ * @param type the credential type determined by the credential-type-specific subclass for
+ * custom use cases
+ * @param candidateQueryData the partial request data in the [Bundle] format that will be sent
+ * to the provider during the initial candidate query stage, which should not contain sensitive
+ * user credential information (note: bundle keys in the form of `androidx.credentials.*` are
+ * reserved for internal library use)
+ * @param callingAppInfo info pertaining to the app that is requesting for credentials
+ * retrieval or creation
+ * @throws IllegalArgumentException If [type] is empty
+ * @throws NullPointerException If [type], or [candidateQueryData] is null
+ */
+open class BeginCreateCustomCredentialRequest constructor(
+ type: String,
+ candidateQueryData: Bundle,
+ callingAppInfo: CallingAppInfo?
+) : BeginCreateCredentialRequest(type, candidateQueryData, callingAppInfo)
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreatePasswordCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreatePasswordCredentialRequest.kt
new file mode 100644
index 0000000..41e4a03
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreatePasswordCredentialRequest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.provider
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.os.Bundle
+import android.service.credentials.CallingAppInfo
+import androidx.credentials.CreatePasswordRequest
+import androidx.credentials.PasswordCredential
+import androidx.credentials.internal.FrameworkClassParsingException
+
+/**
+ * Request to begin saving a password credential, received by the provider with a
+ * CredentialProviderBaseService.onBeginCreateCredentialRequest call.
+ *
+ * This request will not contain all parameters needed to store the password. Provider must
+ * use the initial parameters to determine if the password can be stored, and return
+ * a list of [CreateEntry], denoting the accounts/groups where the password can be stored.
+ * When user selects one of the returned [CreateEntry], the corresponding [PendingIntent] set on
+ * the [CreateEntry] will be fired. The [Intent] invoked through the [PendingIntent] will contain the
+ * complete [CreatePasswordRequest]. This request will contain all required parameters to
+ * actually store the password.
+ *
+ * @see BeginCreateCredentialRequest
+ *
+ * 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 BeginCreatePasswordCredentialRequest constructor(
+ callingAppInfo: CallingAppInfo?,
+ candidateQueryData: Bundle
+) : BeginCreateCredentialRequest(
+ PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
+ candidateQueryData,
+ callingAppInfo,
+) {
+
+ /** @hide **/
+ @Suppress("AcronymName")
+ companion object {
+ /** @hide **/
+ @JvmStatic
+ @Suppress("UNUSED_PARAMETER")
+ internal fun createFrom(data: Bundle, callingAppInfo: CallingAppInfo?):
+ BeginCreatePasswordCredentialRequest {
+ try {
+ return BeginCreatePasswordCredentialRequest(
+ callingAppInfo, data)
+ } catch (e: Exception) {
+ throw FrameworkClassParsingException()
+ }
+ }
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreatePublicKeyCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreatePublicKeyCredentialRequest.kt
new file mode 100644
index 0000000..e68d9fb
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreatePublicKeyCredentialRequest.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.credentials.provider
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.os.Bundle
+import android.service.credentials.CallingAppInfo
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CreatePublicKeyCredentialRequest.Companion.BUNDLE_KEY_REQUEST_JSON
+import androidx.credentials.CreatePublicKeyCredentialRequest.Companion.BUNDLE_KEY_CLIENT_DATA_HASH
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.internal.FrameworkClassParsingException
+
+/**
+ * Request to begin registering a public key credential.
+ *
+ * This request will not contain all parameters needed to create the public key. Provider must
+ * use the initial parameters to determine if the public key can be registered, and return
+ * a list of [CreateEntry], denoting the accounts/groups where the public key can be registered.
+ * When user selects one of the returned [CreateEntry], the corresponding [PendingIntent] set on
+ * the [CreateEntry] will be fired. The [Intent] invoked through the [PendingIntent] will contain
+ * the complete [CreatePublicKeyCredentialRequest]. This request will contain all required
+ * parameters to actually register a public key.
+ *
+ * @property requestJson the request json to be used for registering the public key credential
+ * @property clientDataHash a hash that is used to verify the relying party identity, set only if
+ * [android.service.credentials.CallingAppInfo.getOrigin] is set
+ *
+ * @see BeginCreateCredentialRequest
+ *
+ * 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 BeginCreatePublicKeyCredentialRequest @JvmOverloads constructor(
+ val requestJson: String,
+ callingAppInfo: CallingAppInfo?,
+ candidateQueryData: Bundle,
+ val clientDataHash: ByteArray? = null,
+) : BeginCreateCredentialRequest(
+ PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
+ candidateQueryData,
+ callingAppInfo
+) {
+ init {
+ require(requestJson.isNotEmpty()) { "json must not be empty" }
+ initiateBundle(candidateQueryData, requestJson)
+ }
+
+ private fun initiateBundle(candidateQueryData: Bundle, requestJson: String) {
+ candidateQueryData.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
+ }
+
+ /** @hide **/
+ @Suppress("AcronymName")
+ companion object {
+ /** @hide */
+ @JvmStatic
+ internal fun createFrom(data: Bundle, callingAppInfo: CallingAppInfo?):
+ BeginCreatePublicKeyCredentialRequest {
+ try {
+ val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
+ val clientDataHash = data.getByteArray(BUNDLE_KEY_CLIENT_DATA_HASH)
+ return BeginCreatePublicKeyCredentialRequest(requestJson!!,
+ callingAppInfo, data, clientDataHash)
+ } catch (e: Exception) {
+ throw FrameworkClassParsingException()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialOption.kt
new file mode 100644
index 0000000..fe7811e
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialOption.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.credentials.provider
+
+import android.os.Bundle
+import androidx.credentials.PasswordCredential
+import androidx.credentials.PublicKeyCredential
+
+/**
+ * Base class that a credential provider receives during the query phase of a get-credential flow.
+ * Classes derived from this base class contain
+ * parameters required to retrieve a specific type of credential. E.g. [BeginGetPasswordOption]
+ * contains parameters required to retrieve passwords.
+ *
+ * [BeginGetCredentialRequest] will be composed of a list of [BeginGetCredentialOption]
+ * subclasses to indicate the specific credential types and configurations that the credential
+ * provider must include while building the [BeginGetCredentialResponse].
+ *
+ * @property id unique id representing this particular option. Credential providers must
+ * use this Id while constructing the [CredentialEntry] to be set on [BeginGetCredentialResponse]
+ * @property type the type of the credential to be retrieved against this option. E.g. a
+ * [BeginGetPasswordOption] will have type [PasswordCredential.TYPE_PASSWORD_CREDENTIAL]
+ * @property candidateQueryData the parameters needed to retrieve the credentials, in the form of a
+ * [Bundle]. Note that this is a 'Begin' request denoting a query phase. In this phase, only
+ * sensitive information is included in the [candidateQueryData] bundle.
+ */
+abstract class BeginGetCredentialOption internal constructor(
+ val id: String,
+ val type: String,
+ val candidateQueryData: Bundle
+) {
+ /** @hide **/
+ companion object {
+ @JvmStatic
+ internal fun createFrom(id: String, type: String, candidateQueryData: Bundle):
+ BeginGetCredentialOption {
+ return when (type) {
+ PasswordCredential.TYPE_PASSWORD_CREDENTIAL -> {
+ BeginGetPasswordOption.createFrom(candidateQueryData, id)
+ }
+
+ PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL -> {
+ BeginGetPublicKeyCredentialOption.createFrom(candidateQueryData, id)
+ }
+
+ else -> {
+ BeginGetCustomCredentialOption(id, type, candidateQueryData)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialRequest.kt
new file mode 100644
index 0000000..0a04264
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialRequest.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.credentials.provider
+
+import android.os.Bundle
+import android.service.credentials.CallingAppInfo
+import androidx.annotation.DoNotInline
+import androidx.annotation.OptIn
+import androidx.annotation.RequiresApi
+import androidx.core.os.BuildCompat
+import androidx.credentials.provider.utils.BeginGetCredentialUtil
+
+/**
+ * Query stage request for getting user's credentials from a given credential provider.
+ *
+ * <p>This request contains a list of [BeginGetCredentialOption] that have parameters
+ * to be used to query credentials, and return a list of [CredentialEntry] to be set
+ * on the [BeginGetCredentialResponse]. This list is then shown to the user on a selector.
+ *
+ * @param beginGetCredentialOptions the list of type specific credential options to to be processed
+ * in order to produce a [BeginGetCredentialResponse]
+ * @param callingAppInfo info pertaining to the app requesting credentials
+ */
+class BeginGetCredentialRequest @JvmOverloads constructor(
+ val beginGetCredentialOptions: List<BeginGetCredentialOption>,
+ val callingAppInfo: CallingAppInfo? = null,
+) {
+ @RequiresApi(34)
+ private object Api34Impl {
+ private const val REQUEST_KEY = "androidx.credentials.provider.BeginGetCredentialRequest"
+
+ @JvmStatic
+ @DoNotInline
+ fun writeToBundle(bundle: Bundle, request: BeginGetCredentialRequest) {
+ bundle.putParcelable(
+ REQUEST_KEY,
+ BeginGetCredentialUtil.convertToFrameworkRequest(request)
+ )
+ }
+
+ @JvmStatic
+ @DoNotInline
+ fun readFromBundle(bundle: Bundle): BeginGetCredentialRequest? {
+ val frameworkRequest = bundle.getParcelable(
+ REQUEST_KEY,
+ android.service.credentials.BeginGetCredentialRequest::class.java
+ )
+ if (frameworkRequest != null) {
+ return BeginGetCredentialUtil.convertToJetpackRequest(frameworkRequest)
+ }
+ return null
+ }
+ }
+
+ companion object {
+ /**
+ * Helper method to convert the class to a parcelable [Bundle], in case the class
+ * instance needs to be sent across a process. Consumers of this method should use
+ * [readFromBundle] to reconstruct the class instance back from the bundle returned here.
+ */
+ @JvmStatic
+ @OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
+ fun writeToBundle(request: BeginGetCredentialRequest): Bundle {
+ val bundle = Bundle()
+ if (BuildCompat.isAtLeastU()) {
+ Api34Impl.writeToBundle(bundle, request)
+ }
+ return bundle
+ }
+
+ /**
+ * Helper method to convert a [Bundle] retrieved through [writeToBundle], back
+ * to an instance of [BeginGetCredentialRequest].
+ */
+ @JvmStatic
+ @OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
+ fun readFromBundle(bundle: Bundle): BeginGetCredentialRequest? {
+ if (BuildCompat.isAtLeastU()) {
+ return Api34Impl.readFromBundle(bundle)
+ }
+ return null
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialResponse.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialResponse.kt
new file mode 100644
index 0000000..7e631f4
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialResponse.kt
@@ -0,0 +1,214 @@
+/*
+ * 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.credentials.provider
+
+import android.os.Bundle
+import androidx.annotation.DoNotInline
+import androidx.annotation.OptIn
+import androidx.annotation.RequiresApi
+import androidx.core.os.BuildCompat
+import androidx.credentials.provider.utils.BeginGetCredentialUtil
+
+/**
+ * Response from a credential provider to [BeginGetCredentialRequest], containing credential
+ * entries and other associated data to be shown on the account selector UI.
+ *
+ * Credential providers can set multiple [CredentialEntry] per [BeginGetCredentialOption]
+ * retrieved from the top level request [BeginGetCredentialRequest]. These entries will appear
+ * to the user on the selector.
+ *
+ * Additionally credential providers can add a list of [AuthenticationAction] if all
+ * credentials for the credential provider are locked. Providers can also set a list of
+ * [Action] that can navigate the user straight to a provider activity where the rest of
+ * the request can be processed.
+ */
+class BeginGetCredentialResponse constructor(
+ val credentialEntries: List<CredentialEntry> = listOf(),
+ val actions: List<Action> = listOf(),
+ val authenticationActions: List<AuthenticationAction> = listOf(),
+ val remoteEntry: RemoteEntry? = null
+) {
+ /** Builder for [BeginGetCredentialResponse]. **/
+ class Builder {
+ private var credentialEntries: MutableList<CredentialEntry> = mutableListOf()
+ private var actions: MutableList<Action> = mutableListOf()
+ private var authenticationActions: MutableList<AuthenticationAction> = mutableListOf()
+ private var remoteEntry: RemoteEntry? = null
+
+ /**
+ * Sets a remote credential entry to be shown on the UI. Provider must set this if they
+ * wish to get the credential from a different device.
+ *
+ * When constructing the [CredentialEntry] object, the pending intent
+ * must be set such that it leads to an activity that can provide UI to fulfill the request
+ * on a remote device. When user selects this [remoteEntry], the system will
+ * invoke the pending intent set on the [CredentialEntry].
+ *
+ * <p> Once the remote credential flow is complete, the [android.app.Activity]
+ * result should be set to [android.app.Activity#RESULT_OK] and an extra with the
+ * [CredentialProviderService#EXTRA_GET_CREDENTIAL_RESPONSE] key should be populated
+ * with a [android.credentials.Credential] object.
+ *
+ * <p> Note that as a provider service you will only be able to set a remote entry if :
+ * - Provider service possesses the
+ * [android.Manifest.permission.PROVIDE_REMOTE_CREDENTIALS] permission.
+ * - Provider service is configured as the provider that can provide remote entries.
+ *
+ * If the above conditions are not met, setting back [BeginGetCredentialResponse]
+ * on the callback from [CredentialProviderService#onBeginGetCredential] will
+ * throw a [SecurityException].
+ */
+ fun setRemoteEntry(remoteEntry: RemoteEntry?): Builder {
+ this.remoteEntry = remoteEntry
+ return this
+ }
+
+ /**
+ * Adds a [CredentialEntry] to the list of entries to be displayed on the UI.
+ */
+ fun addCredentialEntry(entry: CredentialEntry): Builder {
+ credentialEntries.add(entry)
+ return this
+ }
+
+ /**
+ * Sets the list of credential entries to be displayed on the account selector UI.
+ */
+ fun setCredentialEntries(entries: List<CredentialEntry>): Builder {
+ credentialEntries = entries.toMutableList()
+ return this
+ }
+
+ /**
+ * Adds an [Action] to the list of actions to be displayed on
+ * the UI.
+ *
+ * <p> An [Action] must be used for independent user actions,
+ * such as opening the app, intenting directly into a certain app activity etc. The
+ * pending intent set with the [action] must invoke the corresponding activity.
+ */
+ fun addAction(action: Action): Builder {
+ this.actions.add(action)
+ return this
+ }
+
+ /**
+ * Sets the list of actions to be displayed on the UI.
+ */
+ fun setActions(actions: List<Action>): Builder {
+ this.actions = actions.toMutableList()
+ return this
+ }
+
+ /**
+ * Add an authentication entry to be shown on the UI. Providers must set this entry if
+ * the corresponding account is locked and no underlying credentials can be returned.
+ *
+ * <p> When the user selects this [authenticationAction], the system invokes the
+ * corresponding pending intent.
+ * Once the authentication action activity is launched, and the user is authenticated,
+ * providers should create another response with [BeginGetCredentialResponse] using
+ * this time adding the unlocked credentials in the form of [CredentialEntry]'s.
+ *
+ * <p>The new response object must be set on the authentication activity's
+ * result. The result code should be set to [android.app.Activity#RESULT_OK] and
+ * the [CredentialProviderService#EXTRA_BEGIN_GET_CREDENTIAL_RESPONSE] extra
+ * should be set with the new fully populated [BeginGetCredentialResponse] object.
+ */
+ fun addAuthenticationAction(authenticationAction: AuthenticationAction): Builder {
+ this.authenticationActions.add(authenticationAction)
+ return this
+ }
+
+ /**
+ * Sets the list of authentication entries to be displayed on the account selector UI.
+ */
+ fun setAuthenticationActions(authenticationEntries: List<AuthenticationAction>): Builder {
+ this.authenticationActions = authenticationEntries.toMutableList()
+ return this
+ }
+
+ /**
+ * Builds a [BeginGetCredentialResponse] instance.
+ */
+ fun build(): BeginGetCredentialResponse {
+ return BeginGetCredentialResponse(
+ credentialEntries.toList(),
+ actions.toList(),
+ authenticationActions.toList(),
+ remoteEntry
+ )
+ }
+ }
+
+ @RequiresApi(34)
+ private object Api34Impl {
+ private const val REQUEST_KEY = "androidx.credentials.provider.BeginGetCredentialResponse"
+
+ @JvmStatic
+ @DoNotInline
+ fun writeToBundle(bundle: Bundle, response: BeginGetCredentialResponse) {
+ bundle.putParcelable(
+ REQUEST_KEY,
+ BeginGetCredentialUtil.convertToFrameworkResponse(response)
+ )
+ }
+
+ @JvmStatic
+ @DoNotInline
+ fun readFromBundle(bundle: Bundle): BeginGetCredentialResponse? {
+ val frameworkResponse = bundle.getParcelable(
+ REQUEST_KEY,
+ android.service.credentials.BeginGetCredentialResponse::class.java
+ )
+ if (frameworkResponse != null) {
+ return BeginGetCredentialUtil.convertToJetpackResponse(frameworkResponse)
+ }
+ return null
+ }
+ }
+
+ companion object {
+ /**
+ * Helper method to convert the class to a parcelable [Bundle], in case the class
+ * instance needs to be sent across a process. Consumers of this method should use
+ * [readFromBundle] to reconstruct the class instance back from the bundle returned here.
+ */
+ @JvmStatic
+ @OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
+ fun writeToBundle(response: BeginGetCredentialResponse): Bundle {
+ val bundle = Bundle()
+ if (BuildCompat.isAtLeastU()) {
+ Api34Impl.writeToBundle(bundle, response)
+ }
+ return bundle
+ }
+
+ /**
+ * Helper method to convert a [Bundle] retrieved through [writeToBundle], back
+ * to an instance of [BeginGetCredentialResponse].
+ */
+ @JvmStatic
+ @OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
+ fun readFromBundle(bundle: Bundle): BeginGetCredentialResponse? {
+ if (BuildCompat.isAtLeastU()) {
+ return Api34Impl.readFromBundle(bundle)
+ }
+ return null
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCustomCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCustomCredentialOption.kt
new file mode 100644
index 0000000..9c2851d
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCustomCredentialOption.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.provider
+
+import android.os.Bundle
+
+/**
+ * Allows extending custom versions of BeginGetCredentialOptions for unique use cases.
+ *
+ * If you get a [BeginGetCustomCredentialOption] instead of a type-safe option class such as
+ * [BeginGetPasswordOption], [BeginGetPublicKeyCredentialOption], etc., then you should check if
+ * you have any other library at interest that supports this custom [type] of credential option,
+ * and if so use its parsing utilities to resolve to a type-safe class within that library.
+ *
+ * @property type the credential type determined by the credential-type-specific subclass
+ * generated for custom use cases
+ * @property candidateQueryData the partial request data in the [Bundle] format that will be sent to
+ * the provider during the initial candidate query stage, which should not contain sensitive user
+ * information
+ * @throws IllegalArgumentException If [type] is null or, empty
+ */
+open class BeginGetCustomCredentialOption constructor(
+ id: String,
+ type: String,
+ candidateQueryData: Bundle,
+) : BeginGetCredentialOption(
+ id,
+ type,
+ candidateQueryData
+) {
+ init {
+ require(id.isNotEmpty()) { "id should not be empty" }
+ require(type.isNotEmpty()) { "type should not be empty" }
+ }
+
+ /** @hide **/
+ @Suppress("AcronymName")
+ companion object {
+ /** @hide */
+ @JvmStatic
+ internal fun createFrom(
+ data: Bundle,
+ id: String,
+ type: String
+ ): BeginGetCustomCredentialOption {
+ return BeginGetCustomCredentialOption(id, type, data)
+ }
+
+ /** @hide */
+ @JvmStatic
+ internal fun createFromEntrySlice(
+ data: Bundle,
+ id: String,
+ type: String
+ ): BeginGetCustomCredentialOption {
+ return BeginGetCustomCredentialOption(id, type, data)
+ }
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetPasswordOption.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetPasswordOption.kt
new file mode 100644
index 0000000..80e30a4
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetPasswordOption.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.provider
+
+import android.os.Bundle
+import android.service.credentials.BeginGetCredentialResponse
+import androidx.credentials.GetPasswordOption
+import androidx.credentials.PasswordCredential
+
+/**
+ * A request to a password provider to begin the flow of retrieving the user's saved passwords.
+ *
+ * Providers must use the parameters in this option to retrieve the corresponding credentials'
+ * metadata, and then return them in the form of a list of [PasswordCredentialEntry]
+ * set on the [BeginGetCredentialResponse].
+ *
+ * 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.
+ *
+ * @property allowedUserIds a optional set of user ids with which the credentials associated are
+ * requested; left as empty if the caller app wants to request all the available user credentials
+ */
+class BeginGetPasswordOption constructor(
+ val allowedUserIds: Set<String>,
+ candidateQueryData: Bundle,
+ id: String,
+) : BeginGetCredentialOption(
+ id,
+ PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
+ candidateQueryData
+) {
+
+ /** @hide **/
+ @Suppress("AcronymName")
+ companion object {
+ /** @hide */
+ @JvmStatic
+ internal fun createFrom(data: Bundle, id: String): BeginGetPasswordOption {
+ val allowUserIdList = data.getStringArrayList(
+ GetPasswordOption.BUNDLE_KEY_ALLOWED_USER_IDS)
+ return BeginGetPasswordOption(allowUserIdList?.toSet() ?: emptySet(), data, id)
+ }
+
+ /** @hide */
+ @JvmStatic
+ internal fun createFromEntrySlice(data: Bundle, id: String): BeginGetPasswordOption {
+ val allowUserIdList = data.getStringArrayList(
+ GetPasswordOption.BUNDLE_KEY_ALLOWED_USER_IDS)
+ return BeginGetPasswordOption(allowUserIdList?.toSet() ?: emptySet(), data, id)
+ }
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOption.kt
new file mode 100644
index 0000000..1ecba47
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOption.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.provider
+
+import android.os.Bundle
+import androidx.credentials.GetPublicKeyCredentialOption
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.internal.FrameworkClassParsingException
+
+/**
+ * A request to begin the flow of getting passkeys from the user's public key credential provider.
+ *
+ * @property requestJson the request in JSON format in the standard webauthn web json
+ * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson)
+ * @property clientDataHash a hash that is used to verify the relying party identity, set only if
+ * [android.service.credentials.CallingAppInfo.getOrigin] is set
+ * @param requestJson the request in JSON format in the standard webauthn web json
+ * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson)
+ * @param clientDataHash a hash that is used to verify the relying party identity, set only if
+ * [android.service.credentials.CallingAppInfo.getOrigin] is set
+ * @param id the id of this request option
+ * @param candidateQueryData the request data in the [Bundle] format
+ * @throws NullPointerException If [requestJson] is null
+ * @throws IllegalArgumentException If [requestJson] is empty
+ *
+ * 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 BeginGetPublicKeyCredentialOption @JvmOverloads constructor(
+ candidateQueryData: Bundle,
+ id: String,
+ val requestJson: String,
+ val clientDataHash: ByteArray? = null,
+) : BeginGetCredentialOption(
+ id,
+ PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
+ candidateQueryData
+) {
+ init {
+ require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
+ }
+
+ /** @hide **/
+ @Suppress("AcronymName")
+ companion object {
+ /** @hide */
+ @JvmStatic
+ internal fun createFrom(data: Bundle, id: String): BeginGetPublicKeyCredentialOption {
+ try {
+ val requestJson = data.getString(GetPublicKeyCredentialOption
+ .BUNDLE_KEY_REQUEST_JSON)
+ val clientDataHash = data.getByteArray(GetPublicKeyCredentialOption
+ .BUNDLE_KEY_CLIENT_DATA_HASH)
+ return BeginGetPublicKeyCredentialOption(data, id, requestJson!!, clientDataHash)
+ } catch (e: Exception) {
+ throw FrameworkClassParsingException()
+ }
+ }
+
+ /** @hide */
+ @JvmStatic
+ internal fun createFromEntrySlice(data: Bundle, id: String):
+ BeginGetPublicKeyCredentialOption {
+ val requestJson = "dummy_request_json"
+ return BeginGetPublicKeyCredentialOption(data, id, requestJson)
+ }
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt
new file mode 100644
index 0000000..898541c9
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt
@@ -0,0 +1,405 @@
+/*
+ * 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.credentials.provider
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.app.slice.Slice
+import android.app.slice.SliceSpec
+import android.graphics.drawable.Icon
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.credentials.CredentialManager
+import androidx.credentials.PasswordCredential
+import androidx.credentials.PublicKeyCredential
+import java.time.Instant
+import java.util.Collections
+
+/**
+ * An entry to be shown on the selector during a create flow initiated when an app calls
+ * [CredentialManager.createCredential]
+ *
+ * A [CreateEntry] points to a location such as an account, or a group where the credential can be
+ * registered. When user selects this entry, the corresponding [PendingIntent] is fired, and the
+ * credential creation can be completed.
+ *
+ * @throws IllegalArgumentException If [accountName] is empty
+ */
+class CreateEntry internal constructor(
+ val accountName: CharSequence,
+ val pendingIntent: PendingIntent,
+ val icon: Icon?,
+ val description: CharSequence?,
+ val lastUsedTime: Instant?,
+ private val credentialCountInformationMap: MutableMap<String, Int?>
+) {
+
+ /**
+ * 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 user selects this entry
+ * @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
+ * @param lastUsedTime the last time the account underlying this entry was used by the user
+ * @param passwordCredentialCount the no. of password credentials saved by the provider
+ * @param publicKeyCredentialCount the no. of public key credentials saved by the provider
+ * @param totalCredentialCount the total no. of credentials saved by the provider
+ *
+ * @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
+ */
+ 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
+ ) : 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
+ )
+ )
+
+ init {
+ require(accountName.isNotEmpty()) { "accountName must not be empty" }
+ if (description != null) {
+ require(description.length <= DESCRIPTION_MAX_CHAR_LIMIT) {
+ "Description must follow a limit of 300 characters."
+ }
+ }
+ }
+
+ /** Returns the no. of password type credentials that the provider with this entry has. */
+ @Suppress("AutoBoxing")
+ fun getPasswordCredentialCount(): Int? {
+ return credentialCountInformationMap[PasswordCredential.TYPE_PASSWORD_CREDENTIAL]
+ }
+
+ /** Returns the no. of public key type credentials that the provider with this entry has. */
+ @Suppress("AutoBoxing")
+ fun getPublicKeyCredentialCount(): Int? {
+ return credentialCountInformationMap[PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL]
+ }
+
+ /** Returns the no. of total credentials that the provider with this entry has.
+ *
+ * This total count is not necessarily equal to the sum of [getPasswordCredentialCount]
+ * and [getPublicKeyCredentialCount].
+ *
+ */
+ @Suppress("AutoBoxing")
+ fun getTotalCredentialCount(): Int? {
+ return credentialCountInformationMap[TYPE_TOTAL_CREDENTIAL]
+ }
+
+ /**
+ * A builder for [CreateEntry]
+ *
+ * @param accountName the name of the account where the credential will be registered
+ * @param pendingIntent the [PendingIntent] that will be fired when the user selects
+ * this entry
+ */
+ 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
+ private var lastUsedTime: Instant? = null
+ private var passwordCredentialCount: Int? = null
+ private var publicKeyCredentialCount: Int? = null
+ private var totalCredentialCount: Int? = null
+
+ /** Sets the password credential count, denoting how many credentials of type
+ * [PasswordCredential.TYPE_PASSWORD_CREDENTIAL] does the provider have stored.
+ *
+ * This information will be displayed on the [CreateEntry] to help the user
+ * make a choice.
+ */
+ fun setPasswordCredentialCount(count: Int): Builder {
+ passwordCredentialCount = count
+ credentialCountInformationMap[PasswordCredential.TYPE_PASSWORD_CREDENTIAL] = count
+ return this
+ }
+
+ /** Sets the password credential count, denoting how many credentials of type
+ * [PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL] does the provider have stored.
+ *
+ * This information will be displayed on the [CreateEntry] to help the user
+ * make a choice.
+ */
+ fun setPublicKeyCredentialCount(count: Int): Builder {
+ publicKeyCredentialCount = count
+ credentialCountInformationMap[PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL] = count
+ return this
+ }
+
+ /** Sets the total credential count, denoting how many credentials in total
+ * does the provider have stored.
+ *
+ * This total count no. does not need to be a total of the counts set through
+ * [setPasswordCredentialCount] and [setPublicKeyCredentialCount].
+ *
+ * This information will be displayed on the [CreateEntry] to help the user
+ * make a choice.
+ */
+ fun setTotalCredentialCount(count: Int): Builder {
+ totalCredentialCount = count
+ credentialCountInformationMap[TYPE_TOTAL_CREDENTIAL] = count
+ return this
+ }
+
+ /** Sets an icon to be displayed with the entry on the UI */
+ fun setIcon(icon: Icon?): Builder {
+ this.icon = icon
+ return this
+ }
+
+ /**
+ * Sets a localized description to be displayed on the UI at the time of credential
+ * creation.
+ *
+ * Typically this description should contain information informing the user of the
+ * credential being created, and where it is being stored. Providers are free
+ * to phrase this however they see fit.
+ *
+ * @throws IllegalArgumentException if [description] is longer than 300 characters (
+ * important: make sure your descriptions across all locales are within this limit).
+ */
+ fun setDescription(description: CharSequence?): Builder {
+ if (description?.length != null && description.length > DESCRIPTION_MAX_CHAR_LIMIT) {
+ throw IllegalArgumentException("Description must follow a limit of 300 characters.")
+ }
+ this.description = description
+ return this
+ }
+
+ /** Sets the last time this account was used */
+ fun setLastUsedTime(lastUsedTime: Instant?): Builder {
+ this.lastUsedTime = lastUsedTime
+ return this
+ }
+
+ /**
+ * Builds an instance of [CreateEntry]
+ *
+ * @throws IllegalArgumentException If [accountName] is empty
+ */
+ fun build(): CreateEntry {
+ return CreateEntry(
+ accountName, pendingIntent, icon, description, lastUsedTime,
+ credentialCountInformationMap
+ )
+ }
+ }
+
+ /** @hide **/
+ @Suppress("AcronymName")
+ companion object {
+ private const val TAG = "CreateEntry"
+ private const val DESCRIPTION_MAX_CHAR_LIMIT = 300
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val TYPE_TOTAL_CREDENTIAL = "TOTAL_CREDENTIAL_COUNT_TYPE"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_ACCOUNT_NAME =
+ "androidx.credentials.provider.createEntry.SLICE_HINT_USER_PROVIDER_ACCOUNT_NAME"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_NOTE =
+ "androidx.credentials.provider.createEntry.SLICE_HINT_NOTE"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_ICON =
+ "androidx.credentials.provider.createEntry.SLICE_HINT_PROFILE_ICON"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_CREDENTIAL_COUNT_INFORMATION =
+ "androidx.credentials.provider.createEntry.SLICE_HINT_CREDENTIAL_COUNT_INFORMATION"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_LAST_USED_TIME_MILLIS =
+ "androidx.credentials.provider.createEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_PENDING_INTENT =
+ "androidx.credentials.provider.createEntry.SLICE_HINT_PENDING_INTENT"
+
+ /** @hide **/
+ @RequiresApi(28)
+ @JvmStatic
+ fun toSlice(
+ createEntry: CreateEntry
+ ): Slice {
+ val accountName = createEntry.accountName
+ val icon = createEntry.icon
+ val description = createEntry.description
+ val lastUsedTime = createEntry.lastUsedTime
+ val credentialCountInformationMap = createEntry.credentialCountInformationMap
+ val pendingIntent = createEntry.pendingIntent
+ // TODO("Use the right type and revision")
+ val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))
+ sliceBuilder.addText(
+ accountName, /*subType=*/null,
+ listOf(SLICE_HINT_ACCOUNT_NAME)
+ )
+ if (lastUsedTime != null) {
+ sliceBuilder.addLong(
+ lastUsedTime.toEpochMilli(), /*subType=*/null, listOf(
+ SLICE_HINT_LAST_USED_TIME_MILLIS
+ )
+ )
+ }
+ if (description != null) {
+ sliceBuilder.addText(
+ description, null,
+ listOf(SLICE_HINT_NOTE)
+ )
+ }
+ if (icon != null) {
+ sliceBuilder.addIcon(
+ icon, /*subType=*/null,
+ listOf(SLICE_HINT_ICON)
+ )
+ }
+ val credentialCountBundle = convertCredentialCountInfoToBundle(
+ credentialCountInformationMap
+ )
+ if (credentialCountBundle != null) {
+ sliceBuilder.addBundle(
+ convertCredentialCountInfoToBundle(
+ credentialCountInformationMap
+ ), null, listOf(
+ SLICE_HINT_CREDENTIAL_COUNT_INFORMATION
+ )
+ )
+ }
+ sliceBuilder.addAction(
+ pendingIntent,
+ Slice.Builder(sliceBuilder)
+ .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
+ .build(),
+ /*subType=*/null
+ )
+ return sliceBuilder.build()
+ }
+
+ /**
+ * Returns an instance of [CreateEntry] derived from a [Slice] object.
+ *
+ * @param slice the [Slice] object constructed through [toSlice]
+ *
+ * @hide
+ */
+ @RequiresApi(28)
+ @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+ @JvmStatic
+ fun fromSlice(slice: Slice): CreateEntry? {
+ // TODO("Put the right spec and version value")
+ var accountName: CharSequence? = null
+ var icon: Icon? = null
+ var pendingIntent: PendingIntent? = null
+ var credentialCountInfo: MutableMap<String, Int?> = mutableMapOf()
+ var description: CharSequence? = null
+ var lastUsedTime: Instant? = null
+ slice.items.forEach {
+ if (it.hasHint(SLICE_HINT_ACCOUNT_NAME)) {
+ accountName = 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_CREDENTIAL_COUNT_INFORMATION)) {
+ credentialCountInfo = convertBundleToCredentialCountInfo(it.bundle)
+ as MutableMap<String, Int?>
+ } else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {
+ lastUsedTime = Instant.ofEpochMilli(it.long)
+ } else if (it.hasHint(SLICE_HINT_NOTE)) {
+ description = it.text
+ }
+ }
+ return try {
+ CreateEntry(
+ accountName!!, pendingIntent!!, icon, description,
+ lastUsedTime, credentialCountInfo
+ )
+ } catch (e: Exception) {
+ Log.i(TAG, "fromSlice failed with: " + e.message)
+ null
+ }
+ }
+
+ /** @hide **/
+ @JvmStatic
+ internal fun convertBundleToCredentialCountInfo(bundle: Bundle?):
+ Map<String, Int?> {
+ val credentialCountMap = HashMap<String, Int?>()
+ if (bundle == null) {
+ return credentialCountMap
+ }
+ bundle.keySet().forEach {
+ try {
+ credentialCountMap[it] = bundle.getInt(it)
+ } catch (e: Exception) {
+ Log.i(TAG, "Issue unpacking credential count info bundle: " + e.message)
+ }
+ }
+ return credentialCountMap
+ }
+
+ /** @hide **/
+ @JvmStatic
+ internal fun convertCredentialCountInfoToBundle(
+ credentialCountInformationMap: Map<String, Int?>
+ ): Bundle? {
+ var foundCredentialCount = false
+ val bundle = Bundle()
+ credentialCountInformationMap.forEach {
+ if (it.value != null) {
+ bundle.putInt(it.key, it.value!!)
+ foundCredentialCount = true
+ }
+ }
+ if (!foundCredentialCount) {
+ return null
+ }
+ return bundle
+ }
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt
new file mode 100644
index 0000000..4f020ced
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.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.credentials.provider
+
+import android.app.slice.Slice
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.credentials.PasswordCredential.Companion.TYPE_PASSWORD_CREDENTIAL
+import androidx.credentials.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
+
+/**
+ * Base class for a credential entry to be displayed on
+ * the selector.
+ */
+abstract class CredentialEntry internal constructor(
+ /** @hide */
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ open val type: String,
+ val beginGetCredentialOption: BeginGetCredentialOption,
+ /** @hide */
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ val slice: Slice
+) {
+ /** @hide **/
+ companion object {
+ @JvmStatic
+ @RequiresApi(34)
+ internal fun createFrom(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)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderService.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderService.kt
new file mode 100644
index 0000000..27406d7
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderService.kt
@@ -0,0 +1,285 @@
+/*
+ * 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.credentials.provider
+
+import android.app.Activity
+import android.app.PendingIntent
+import android.credentials.ClearCredentialStateException
+import android.credentials.GetCredentialException
+import android.os.CancellationSignal
+import android.os.OutcomeReceiver
+import android.service.credentials.ClearCredentialStateRequest
+import android.service.credentials.CredentialEntry
+import android.service.credentials.CredentialProviderService
+import androidx.annotation.RequiresApi
+import androidx.credentials.exceptions.ClearCredentialException
+import androidx.credentials.exceptions.CreateCredentialException
+import androidx.credentials.provider.utils.BeginCreateCredentialUtil
+import androidx.credentials.provider.utils.BeginGetCredentialUtil
+import androidx.credentials.provider.utils.ClearCredentialUtil
+
+/**
+ * A [CredentialProviderService] is a service used to save and retrieve credentials for a given
+ * user, upon the request of a client app that typically uses these credentials for sign-in flows.
+ *
+ * The credential retrieval and creation/saving is mediated by the Android System that
+ * aggregates credentials from multiple credential provider services, and presents them to
+ * the user in the form of a selector UI for credential selections/account selections/
+ * confirmations etc.
+ *
+ * A [CredentialProviderService] is only bound to the Android System for the span
+ * of a [androidx.credentials.CredentialManager] get/create API call. The service is bound only
+ * if :
+ * 1. The service requires the [android.Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE]
+ * permission.
+ * 2. The user has enabled this service as a credential provider from the
+ * settings.
+ *
+ * ## Basic Usage
+ * The basic Credential Manager flow is as such:
+ * - Client app calls one of the APIs exposed in [androidx.credentials.CredentialManager].
+ * - Android system propagates the developer's request to providers that have been
+ * enabled by the user, and can support the [androidx.credentials.Credential] type
+ * specified in the request. We call this the **query phase** of provider communication.
+ * Developer may specify a different set of request parameters to be sent to the provider
+ * during this phase.
+ * - In this query phase, providers, in most cases, will respond with a list of
+ * [CredentialEntry], and an optional list of [Action] entries (for the get flow), and a list
+ * of [CreateEntry] (for the create flow). No actual credentials will be returned in this phase.
+ * - Provider responses are aggregated and presented to the user in the form of a selector UI.
+ * - User selects an entry on the selector.
+ * - Android System invokes the [PendingIntent] associated with this entry, that belongs to the
+ * corresponding provider. We call this the **final phase** of provider communication. The
+ * [PendingIntent] contains the complete request originally created by the developer.
+ * - Provider finishes the [Activity] invoked by the [PendingIntent] by setting the result
+ * as the activity is finished.
+ * - Android System sends back the result to the client app.
+ *
+ * The flow described above minimizes the amount of time a service is bound to the system.
+ * Calls to the service are considered stateless. If a service wishes to maintain state
+ * between the calls, it must do its own state management.
+ * Note: The service's process might be killed by the Android System when unbound, for cases
+ * such as low memory on the device.
+ *
+ * ## Service Registration
+ * In order for Credential Manager to propagate requests to a given provider service, the provider
+ * must:
+ * - Extend this class and implement the abstract methods.
+ * - Declare the [CredentialProviderService.SERVICE_INTERFACE] intent as handled by the service.
+ * - Require the [android.Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE] permission.
+ * - Declare capabilities that the provider supports. Capabilities are essentially credential types
+ * that the provider can handle. Capabilities must be added to the metadata of the service against
+ * [CredentialProviderService.SERVICE_META_DATA].
+ */
+@RequiresApi(34)
+abstract class CredentialProviderService : CredentialProviderService() {
+
+ final override fun onBeginGetCredential(
+ request: android.service.credentials.BeginGetCredentialRequest,
+ cancellationSignal: CancellationSignal,
+ callback: OutcomeReceiver<
+ android.service.credentials.BeginGetCredentialResponse, GetCredentialException>
+ ) {
+ val structuredRequest = BeginGetCredentialUtil.convertToJetpackRequest(request)
+ val outcome = object : OutcomeReceiver<BeginGetCredentialResponse,
+ androidx.credentials.exceptions.GetCredentialException> {
+ override fun onResult(response: BeginGetCredentialResponse) {
+ callback.onResult(
+ BeginGetCredentialUtil
+ .convertToFrameworkResponse(response)
+ )
+ }
+
+ override fun onError(error: androidx.credentials.exceptions.GetCredentialException) {
+ super.onError(error)
+ // TODO("Change error code to provider error when ready on framework")
+ callback.onError(GetCredentialException(error.type, error.message))
+ }
+ }
+ this.onBeginGetCredentialRequest(structuredRequest, cancellationSignal, outcome)
+ }
+
+ final override fun onBeginCreateCredential(
+ request: android.service.credentials.BeginCreateCredentialRequest,
+ cancellationSignal: CancellationSignal,
+ callback: OutcomeReceiver<android.service.credentials.BeginCreateCredentialResponse,
+ android.credentials.CreateCredentialException>
+ ) {
+ val outcome = object : OutcomeReceiver<
+ BeginCreateCredentialResponse, CreateCredentialException> {
+ override fun onResult(response: BeginCreateCredentialResponse) {
+ callback.onResult(
+ BeginCreateCredentialUtil
+ .convertToFrameworkResponse(response)
+ )
+ }
+
+ override fun onError(error: CreateCredentialException) {
+ super.onError(error)
+ // TODO("Change error code to provider error when ready on framework")
+ callback.onError(
+ android.credentials.CreateCredentialException(
+ error.type, error.message
+ )
+ )
+ }
+ }
+ onBeginCreateCredentialRequest(
+ BeginCreateCredentialUtil.convertToJetpackRequest(request),
+ cancellationSignal, outcome
+ )
+ }
+
+ final override fun onClearCredentialState(
+ request: ClearCredentialStateRequest,
+ cancellationSignal: CancellationSignal,
+ callback: OutcomeReceiver<Void, ClearCredentialStateException>
+ ) {
+ val outcome = object : OutcomeReceiver<Void?, ClearCredentialException> {
+ override fun onResult(response: Void?) {
+ callback.onResult(response)
+ }
+
+ override fun onError(error: ClearCredentialException) {
+ super.onError(error)
+ // TODO("Change error code to provider error when ready on framework")
+ callback.onError(ClearCredentialStateException(error.type, error.message))
+ }
+ }
+ onClearCredentialStateRequest(ClearCredentialUtil.convertToJetpackRequest(request),
+ cancellationSignal, outcome)
+ }
+
+ /**
+ * Called by the Android System in response to a client app calling
+ * [androidx.credentials.CredentialManager.clearCredentialState]. A client app typically
+ * calls this API on instances like sign-out when the intention is that the providers clear
+ * any state that they may have maintained for the given user.
+ *
+ * You should invoked this api after your user signs out of your app to notify all credential
+ * providers that any stored credential session for the given app should be cleared.
+ *
+ * An example scenario of a state that is maintained and is expected to be cleared on this
+ * call, is when an active credential session is being stored to limit sign-in options
+ * in the result of subsequent get-request calls. When a user explicitly signs out of the app,
+ * the next time, the client app may want their users to see all options and hence will call
+ * this API first to make sure credential providers can clear the state maintained previously.
+ *
+ * @param request the request for the credential provider to handle
+ * @param cancellationSignal signal for observing cancellation requests. The system will
+ * use this to notify you that the result is no longer needed and you should stop
+ * handling it in order to save your resources
+ * @param callback the callback object to be used to notify the response or error
+ */
+ abstract fun onClearCredentialStateRequest(
+ request: ProviderClearCredentialStateRequest,
+ cancellationSignal: CancellationSignal,
+ callback: OutcomeReceiver<Void?,
+ ClearCredentialException>
+ )
+
+ /**
+ * Called by the Android System in response to a client app calling
+ * [androidx.credentials.CredentialManager.getCredential], to get a credential
+ * sourced from a credential provider installed on the device.
+ *
+ * Credential provider services must extend this method in order to handle a
+ * [BeginGetCredentialRequest] request. Once processed, the service must call one of the
+ * [callback] methods to notify the result of the request.
+ *
+ * This API call is referred to as the **query phase** of the original get request from
+ * the client app. In this phase, provider must go over all the
+ * [android.service.credentials.BeginGetCredentialOption], and add corresponding a
+ * [CredentialEntry] to the [BeginGetCredentialResponse]. Each [CredentialEntry] should
+ * contain meta-data to be shown on the selector UI. In addition, each [CredentialEntry]
+ * must contain a [PendingIntent].
+ * Optionally, providers can also add [Action] entries for any non-credential related actions
+ * that they want to offer to the users e.g. opening app, managing credentials etc.
+ *
+ * When user selects one of the [CredentialEntry], **final phase** of the original client's
+ * get-request flow starts. The Android System attached the complete
+ * [androidx.credentials.provider.ProviderGetCredentialRequest] to an intent extra of the
+ * activity that is started by the pending intent. The request must be retrieved through
+ * [PendingIntentHandler.retrieveProviderGetCredentialRequest]. This final request
+ * will only contain a single [androidx.credentials.CredentialOption] that contains the
+ * parameters of the credential the user has requested. The provider service must retrieve this
+ * credential and return through [PendingIntentHandler.setGetCredentialResponse].
+ *
+ * **Handling locked provider apps**
+ * If the provider app is locked, and the provider cannot provide any meta-data based
+ * [CredentialEntry], provider must set an [AuthenticationAction] on the
+ * [BeginGetCredentialResponse]. The [PendingIntent] set on this entry must lead the user
+ * to an >unlock activity. Once unlocked, the provider must retrieve all credentials,
+ * and set the list of [CredentialEntry] and the list of optional [Action] as a result
+ * of the >unlock activity through [PendingIntentHandler.setBeginGetCredentialResponse].
+ *
+ * @see CredentialEntry for how an entry representing a credential must be built
+ * @see Action for how a non-credential related action should be built
+ * @see AuthenticationAction for how an entry that navigates the user to an unlock flow
+ * can be built
+ *
+ * @param [request] the [ProviderGetCredentialRequest] to handle
+ * See [BeginGetCredentialResponse] for the response to be returned
+ * @param cancellationSignal signal for observing cancellation requests. The system will
+ * use this to notify you that the result is no longer needed and you should stop
+ * handling it in order to save your resources
+ * @param callback the callback object to be used to notify the response or error
+ */
+ abstract fun onBeginGetCredentialRequest(
+ request: BeginGetCredentialRequest,
+ cancellationSignal: CancellationSignal,
+ callback: OutcomeReceiver<BeginGetCredentialResponse,
+ androidx.credentials.exceptions.GetCredentialException>
+ )
+
+ /**
+ * Called by the Android System in response to a client app calling
+ * [androidx.credentials.CredentialManager.createCredential], to create/save a credential
+ * with a credential provider installed on the device.
+ *
+ * Credential provider services must extend this method in order to handle a
+ * [BeginCreateCredentialRequest] request. Once processed, the service must call one of the
+ * [callback] methods to notify the result of the request.
+ *
+ * This API call is referred to as the **query phase** of the original create request from
+ * the client app. In this phase, provider must process the request parameters in the
+ * [BeginCreateCredentialRequest] and return a list of [CreateEntry] whereby every
+ * entry represents an account/group where the user will be storing the credential. Each
+ * [CreateEntry] must contain a [PendingIntent] that will lead the user to an activity
+ * in the credential provider's app that will complete the actual credential creation.
+ *
+ * When user selects one of the [CreateEntry], the associated [PendingIntent] will be invoked
+ * and the provider will receive the complete request as part of the extras in the resulting
+ * activity. Provider must retrieve the request through
+ * [PendingIntentHandler.retrieveProviderCreateCredentialRequest].
+ * Once the activity is complete, and the credential is created, provider must set back the
+ * response through [PendingIntentHandler.setCreateCredentialResponse].
+ *
+ * @param [request] the [BeginCreateCredentialRequest] to handle
+ * See [BeginCreateCredentialResponse] for the response to be returned
+ * @param cancellationSignal signal for observing cancellation requests. The system will
+ * use this to notify you that the result is no longer needed and you should stop
+ * handling it in order to save your resources
+ * @param callback the callback object to be used to notify the response or error
+ */
+ abstract fun onBeginCreateCredentialRequest(
+ request: BeginCreateCredentialRequest,
+ cancellationSignal: CancellationSignal,
+ callback: OutcomeReceiver<BeginCreateCredentialResponse,
+ CreateCredentialException>
+ )
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt
new file mode 100644
index 0000000..75fc027
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt
@@ -0,0 +1,399 @@
+/*
+ * 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.credentials.provider
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.app.slice.Slice
+import android.app.slice.SliceSpec
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.credentials.CredentialOption
+import androidx.credentials.R
+import java.time.Instant
+import java.util.Collections
+
+/**
+ * Custom credential entry for a custom credential tyoe that is displayed on the account
+ * selector UI.
+ *
+ * Each entry corresponds to an account that can provide a credential.
+ *
+ * @property title the title shown with this entry on the selector UI
+ * @property subtitle the subTitle shown with this entry on the selector UI
+ * @property lastUsedTime the last used time the credential underlying this entry was
+ * used by the user
+ * @property 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
+ * @property pendingIntent the [PendingIntent] to be invoked when this entry
+ * is selected by the user
+ * @property typeDisplayName the friendly name to be displayed on the UI for
+ * the type of the credential
+ * @property isAutoSelectAllowed whether this entry is allowed to be auto
+ * selected if it is the only one on the UI. Note that setting this value
+ * to true does not guarantee this behavior. The developer must also set this
+ * to true, and the framework must determine that only one entry is present
+ */
+@RequiresApi(28)
+class CustomCredentialEntry internal constructor(
+ override val type: String,
+ val title: CharSequence,
+ val pendingIntent: PendingIntent,
+ @get:Suppress("AutoBoxing")
+ val isAutoSelectAllowed: Boolean,
+ val subtitle: CharSequence?,
+ val typeDisplayName: CharSequence?,
+ val icon: Icon,
+ val lastUsedTime: Instant?,
+ beginGetCredentialOption: BeginGetCredentialOption,
+ /** @hide */
+ val autoSelectAllowedFromOption: Boolean = false,
+ /** @hide */
+ val isDefaultIcon: Boolean = false
+) : CredentialEntry(
+ type,
+ beginGetCredentialOption,
+ toSlice(
+ type,
+ title,
+ subtitle,
+ pendingIntent,
+ typeDisplayName,
+ lastUsedTime,
+ icon,
+ isAutoSelectAllowed,
+ beginGetCredentialOption
+ )
+) {
+ init {
+ require(type.isNotEmpty()) { "type must not be empty" }
+ require(title.isNotEmpty()) { "title must not be empty" }
+ }
+
+ 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
+ ) : this(
+ beginGetCredentialOption.type,
+ title,
+ pendingIntent,
+ isAutoSelectAllowed,
+ subtitle,
+ typeDisplayName,
+ icon,
+ lastUsedTime,
+ beginGetCredentialOption
+ )
+
+ /** @hide **/
+ @Suppress("AcronymName")
+ companion object {
+ private const val TAG = "CredentialEntry"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_TYPE_DISPLAY_NAME =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_TITLE =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_USER_NAME"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_SUBTITLE =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_CREDENTIAL_TYPE_DISPLAY_NAME"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_LAST_USED_TIME_MILLIS =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_ICON =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_PROFILE_ICON"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_PENDING_INTENT =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_PENDING_INTENT"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_AUTO_ALLOWED =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_ALLOWED"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_OPTION_ID =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_OPTION_ID"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_AUTO_SELECT_FROM_OPTION =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_SELECT_FROM_OPTION"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_DEFAULT_ICON_RES_ID =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEFAULT_ICON_RES_ID"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val AUTO_SELECT_TRUE_STRING = "true"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val AUTO_SELECT_FALSE_STRING = "false"
+
+ /** @hide */
+ @JvmStatic
+ fun toSlice(
+ type: String,
+ title: CharSequence,
+ subtitle: CharSequence?,
+ pendingIntent: PendingIntent,
+ typeDisplayName: CharSequence?,
+ lastUsedTime: Instant?,
+ icon: Icon,
+ isAutoSelectAllowed: Boolean?,
+ beginGetCredentialOption: BeginGetCredentialOption
+ ): Slice {
+ // TODO("Put the right revision value")
+ val autoSelectAllowed = if (isAutoSelectAllowed == true) {
+ AUTO_SELECT_TRUE_STRING
+ } else {
+ AUTO_SELECT_FALSE_STRING
+ }
+ val sliceBuilder = Slice.Builder(
+ Uri.EMPTY, SliceSpec(
+ type, 1
+ )
+ )
+ .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)
+ )
+ .addIcon(
+ icon, /*subType=*/null,
+ listOf(SLICE_HINT_ICON)
+ )
+
+ try {
+ if (icon.resId == R.drawable.ic_other_sign_in) {
+ sliceBuilder.addInt(
+ /*true=*/1,
+ /*subType=*/null,
+ listOf(SLICE_HINT_DEFAULT_ICON_RES_ID)
+ )
+ }
+ } catch (_: IllegalStateException) {
+ }
+
+ if (CredentialOption.extractAutoSelectValue(
+ beginGetCredentialOption.candidateQueryData
+ )
+ ) {
+ sliceBuilder.addInt(
+ /*true=*/1,
+ /*subType=*/null,
+ listOf(SLICE_HINT_AUTO_SELECT_FROM_OPTION)
+ )
+ }
+ if (lastUsedTime != null) {
+ sliceBuilder.addLong(
+ lastUsedTime.toEpochMilli(),
+ /*subType=*/null,
+ listOf(SLICE_HINT_LAST_USED_TIME_MILLIS)
+ )
+ }
+ sliceBuilder.addAction(
+ pendingIntent,
+ Slice.Builder(sliceBuilder)
+ .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
+ .build(),
+ /*subType=*/null
+ )
+ return sliceBuilder.build()
+ }
+
+ /**
+ * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
+ *
+ * @param slice the [Slice] object constructed through [toSlice]
+ *
+ * @hide
+ */
+ @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+ @JvmStatic
+ fun fromSlice(slice: Slice): CustomCredentialEntry? {
+ val type: String = slice.spec!!.type
+ var typeDisplayName: CharSequence? = null
+ var title: CharSequence? = null
+ var subtitle: CharSequence? = null
+ var icon: Icon? = null
+ var pendingIntent: PendingIntent? = null
+ var lastUsedTime: Instant? = null
+ var autoSelectAllowed = false
+ var beginGetCredentialOptionId: CharSequence? = null
+ var autoSelectAllowedFromOption = false
+ var isDefaultIcon = false
+
+ slice.items.forEach {
+ 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
+ } 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)) {
+ 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)) {
+ val autoSelectValue = it.text
+ if (autoSelectValue == AUTO_SELECT_TRUE_STRING) {
+ autoSelectAllowed = true
+ }
+ } else if (it.hasHint(SLICE_HINT_AUTO_SELECT_FROM_OPTION)) {
+ autoSelectAllowedFromOption = true
+ } else if (it.hasHint(SLICE_HINT_DEFAULT_ICON_RES_ID)) {
+ isDefaultIcon = true
+ }
+ }
+
+ return try {
+ CustomCredentialEntry(
+ type,
+ title!!,
+ pendingIntent!!,
+ autoSelectAllowed,
+ subtitle,
+ typeDisplayName,
+ icon!!,
+ lastUsedTime,
+ BeginGetCustomCredentialOption(
+ beginGetCredentialOptionId!!.toString(),
+ type,
+ Bundle()
+ ),
+ autoSelectAllowedFromOption,
+ isDefaultIcon
+ )
+ } catch (e: Exception) {
+ Log.i(TAG, "fromSlice failed with: " + e.message)
+ null
+ }
+ }
+ }
+
+ /** Builder for [CustomCredentialEntry] */
+ class Builder(
+ private val context: Context,
+ private val type: String,
+ private val title: CharSequence,
+ private val pendingIntent: PendingIntent,
+ private val beginGetCredentialOption: BeginGetCredentialOption
+ ) {
+ private var subtitle: CharSequence? = null
+ private var lastUsedTime: Instant? = null
+ private var typeDisplayName: CharSequence? = null
+ private var icon: Icon? = null
+ private var autoSelectAllowed = false
+
+ /** Sets a displayName to be shown on the UI with this entry. */
+ fun setSubtitle(subtitle: CharSequence?): Builder {
+ this.subtitle = subtitle
+ return this
+ }
+
+ /** Sets the display name of this credential type, to be shown on the UI with this entry. */
+ fun setTypeDisplayName(typeDisplayName: CharSequence?): Builder {
+ this.typeDisplayName = typeDisplayName
+ return this
+ }
+
+ /**
+ * Sets the icon to be show on the UI.
+ * If no icon is set, a default icon representing a custom credential will be set.
+ */
+ fun setIcon(icon: Icon): Builder {
+ this.icon = icon
+ return this
+ }
+
+ /**
+ * Sets whether the entry should be auto-selected.
+ * The value is false by default
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ fun setAutoSelectAllowed(autoSelectAllowed: Boolean): Builder {
+ this.autoSelectAllowed = autoSelectAllowed
+ return this
+ }
+
+ /**
+ * Sets the last used time of this account. This information will be used to sort the
+ * entries on the selector.
+ */
+ fun setLastUsedTime(lastUsedTime: Instant?): Builder {
+ this.lastUsedTime = lastUsedTime
+ return this
+ }
+
+ /** Builds an instance of [CustomCredentialEntry] */
+ fun build(): CustomCredentialEntry {
+ if (icon == null) {
+ icon = Icon.createWithResource(context, R.drawable.ic_other_sign_in)
+ }
+ return CustomCredentialEntry(
+ type,
+ title,
+ pendingIntent,
+ autoSelectAllowed,
+ subtitle,
+ typeDisplayName,
+ icon!!,
+ lastUsedTime,
+ beginGetCredentialOption
+ )
+ }
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt
new file mode 100644
index 0000000..117e783
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt
@@ -0,0 +1,389 @@
+/*
+ * 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.credentials.provider
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.app.slice.Slice
+import android.app.slice.SliceSpec
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.credentials.CredentialOption
+import androidx.credentials.PasswordCredential
+import androidx.credentials.R
+import java.time.Instant
+import java.util.Collections
+
+/**
+ * A password credential entry that is displayed on the account selector UI. This
+ * entry denotes that a credential of type [PasswordCredential.TYPE_PASSWORD_CREDENTIAL]
+ * is available for the user to select.
+ *
+ * Once this entry is selected, the corresponding [pendingIntent] will be invoked. The provider
+ * can then show any activity they wish to. Before finishing the activity, provider must
+ * set the final [androidx.credentials.GetCredentialResponse] through the
+ * [PendingIntentHandler.setGetCredentialResponse] helper API.
+ *
+ * @property username the username of the account holding the password credential
+ * @property displayName the displayName of the account holding the password credential
+ * @property lastUsedTime the last used time of this entry
+ * @property 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
+ * @property pendingIntent the [PendingIntent] to be invoked when user selects
+ * this entry
+ * @property isAutoSelectAllowed whether this entry is allowed to be auto
+ * selected if it is the only one on the UI. Note that setting this value
+ * to true does not guarantee this behavior. The developer must also set this
+ * to true, and the framework must determine that this is the only entry available for the user.
+ *
+ * @throws IllegalArgumentException if [username] is empty
+ *
+ * @see CustomCredentialEntry
+ */
+@RequiresApi(28)
+class PasswordCredentialEntry internal constructor(
+ val username: CharSequence,
+ val displayName: CharSequence?,
+ val typeDisplayName: CharSequence,
+ val pendingIntent: PendingIntent,
+ val lastUsedTime: Instant?,
+ val icon: Icon,
+ val isAutoSelectAllowed: Boolean,
+ beginGetPasswordOption: BeginGetPasswordOption,
+ /** @hide */
+ val autoSelectAllowedFromOption: Boolean = false,
+ /** @hide */
+ val isDefaultIcon: Boolean = false
+) : CredentialEntry(
+ PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
+ beginGetPasswordOption,
+ toSlice(
+ PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
+ username,
+ displayName,
+ pendingIntent,
+ typeDisplayName,
+ lastUsedTime,
+ icon,
+ isAutoSelectAllowed,
+ beginGetPasswordOption
+ )
+) {
+ init {
+ require(username.isNotEmpty()) { "username must not be empty" }
+ }
+
+ 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
+ ) : this(
+ username,
+ displayName,
+ typeDisplayName = context.getString(
+ R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL
+ ),
+ pendingIntent,
+ lastUsedTime,
+ icon,
+ isAutoSelectAllowed,
+ beginGetPasswordOption,
+ )
+
+ /** @hide **/
+ @Suppress("AcronymName")
+ companion object {
+ private const val TAG = "PasswordCredentialEntry"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_TYPE_DISPLAY_NAME =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_TITLE =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_USER_NAME"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_SUBTITLE =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_CREDENTIAL_TYPE_DISPLAY_NAME"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_DEFAULT_ICON_RES_ID =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEFAULT_ICON_RES_ID"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_LAST_USED_TIME_MILLIS =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_ICON =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_PROFILE_ICON"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_PENDING_INTENT =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_PENDING_INTENT"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_OPTION_ID =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_OPTION_ID"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_AUTO_ALLOWED =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_ALLOWED"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_AUTO_SELECT_FROM_OPTION =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_SELECT_FROM_OPTION"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val AUTO_SELECT_TRUE_STRING = "true"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val AUTO_SELECT_FALSE_STRING = "false"
+
+ /** @hide */
+ @JvmStatic
+ fun toSlice(
+ type: String,
+ title: CharSequence,
+ subTitle: CharSequence?,
+ pendingIntent: PendingIntent,
+ typeDisplayName: CharSequence?,
+ lastUsedTime: Instant?,
+ icon: Icon,
+ isAutoSelectAllowed: Boolean,
+ beginGetPasswordCredentialOption: BeginGetPasswordOption
+ ): Slice {
+ // TODO("Put the right revision value")
+ val autoSelectAllowed = if (isAutoSelectAllowed) {
+ AUTO_SELECT_TRUE_STRING
+ } else {
+ AUTO_SELECT_FALSE_STRING
+ }
+ val sliceBuilder = Slice.Builder(
+ Uri.EMPTY, SliceSpec(
+ type, 1
+ )
+ )
+ .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)
+ )
+ try {
+ if (icon.resId == R.drawable.ic_password) {
+ sliceBuilder.addInt(
+ /*true=*/1,
+ /*subType=*/null,
+ listOf(SLICE_HINT_DEFAULT_ICON_RES_ID)
+ )
+ }
+ } catch (_: IllegalStateException) {
+ }
+
+ if (CredentialOption.extractAutoSelectValue(
+ beginGetPasswordCredentialOption.candidateQueryData
+ )
+ ) {
+ sliceBuilder.addInt(
+ /*true=*/1,
+ /*subType=*/null,
+ listOf(SLICE_HINT_AUTO_SELECT_FROM_OPTION)
+ )
+ }
+ if (lastUsedTime != null) {
+ sliceBuilder.addLong(
+ lastUsedTime.toEpochMilli(),
+ /*subType=*/null,
+ listOf(SLICE_HINT_LAST_USED_TIME_MILLIS)
+ )
+ }
+ sliceBuilder.addAction(
+ pendingIntent,
+ Slice.Builder(sliceBuilder)
+ .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
+ .build(),
+ /*subType=*/null
+ )
+ return sliceBuilder.build()
+ }
+
+ /**
+ * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
+ *
+ * @param slice the [Slice] object constructed through [toSlice]
+ *
+ * @hide
+ */
+ @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+ @JvmStatic
+ fun fromSlice(slice: Slice): PasswordCredentialEntry? {
+ var typeDisplayName: CharSequence? = null
+ var title: 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 isDefaultIcon = false
+
+ slice.items.forEach {
+ 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
+ } 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)) {
+ val autoSelectValue = it.text
+ if (autoSelectValue == AUTO_SELECT_TRUE_STRING) {
+ autoSelectAllowed = true
+ }
+ } else if (it.hasHint(SLICE_HINT_AUTO_SELECT_FROM_OPTION)) {
+ autoSelectAllowedFromOption = true
+ } else if (it.hasHint(SLICE_HINT_DEFAULT_ICON_RES_ID)) {
+ isDefaultIcon = true
+ }
+ }
+
+ return try {
+ PasswordCredentialEntry(
+ title!!,
+ subTitle,
+ typeDisplayName!!,
+ pendingIntent!!,
+ lastUsedTime,
+ icon!!,
+ autoSelectAllowed,
+ BeginGetPasswordOption.createFromEntrySlice(
+ Bundle(),
+ beginGetPasswordOptionId!!.toString()
+ ),
+ autoSelectAllowedFromOption,
+ isDefaultIcon
+ )
+ } catch (e: Exception) {
+ Log.i(TAG, "fromSlice failed with: " + e.message)
+ null
+ }
+ }
+ }
+
+ /** Builder for [PasswordCredentialEntry] */
+ class Builder(
+ private val context: Context,
+ private val username: CharSequence,
+ private val pendingIntent: PendingIntent,
+ private val beginGetPasswordOption: BeginGetPasswordOption
+ ) {
+ private var displayName: CharSequence? = null
+ private var lastUsedTime: Instant? = null
+ private var icon: Icon? = null
+ private var autoSelectAllowed = false
+
+ /** Sets a displayName to be shown on the UI with this entry */
+ fun setDisplayName(displayName: CharSequence?): Builder {
+ this.displayName = displayName
+ return this
+ }
+
+ /** Sets the icon to be shown on the UI with this entry */
+ fun setIcon(icon: Icon): Builder {
+ this.icon = icon
+ return this
+ }
+
+ /**
+ * Sets whether the entry should be auto-selected.
+ * The value is false by default
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ fun setAutoSelectAllowed(autoSelectAllowed: Boolean): Builder {
+ this.autoSelectAllowed = autoSelectAllowed
+ return this
+ }
+
+ /**
+ * Sets the last used time of this account. This information will be used to sort the
+ * entries on the selector.
+ */
+ fun setLastUsedTime(lastUsedTime: Instant?): Builder {
+ this.lastUsedTime = lastUsedTime
+ return this
+ }
+
+ /** Builds an instance of [PasswordCredentialEntry] */
+ fun build(): PasswordCredentialEntry {
+ if (icon == null) {
+ icon = Icon.createWithResource(context, R.drawable.ic_password)
+ }
+ val typeDisplayName = context.getString(
+ R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL
+ )
+ return PasswordCredentialEntry(
+ username,
+ displayName,
+ typeDisplayName,
+ pendingIntent,
+ lastUsedTime,
+ icon!!,
+ autoSelectAllowed,
+ beginGetPasswordOption
+ )
+ }
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
new file mode 100644
index 0000000..9875032
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
@@ -0,0 +1,251 @@
+/*
+ * 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.credentials.provider
+
+import android.app.Activity
+import android.app.PendingIntent
+import android.content.Intent
+import android.service.credentials.BeginCreateCredentialResponse
+import android.service.credentials.CreateCredentialRequest
+import android.service.credentials.CredentialEntry
+import android.service.credentials.CredentialProviderService
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.credentials.CreateCredentialResponse
+import androidx.credentials.GetCredentialResponse
+import androidx.credentials.exceptions.CreateCredentialException
+import androidx.credentials.exceptions.GetCredentialException
+import androidx.credentials.provider.utils.BeginGetCredentialUtil
+
+/**
+ * PendingIntentHandler to be used by credential providers to extract requests from
+ * [PendingIntent] invoked when a given [CreateEntry], or a [CustomCredentialEntry]
+ * is selected by the user.
+ *
+ * This handler can also be used to set [android.credentials.CreateCredentialResponse] and
+ * [android.credentials.GetCredentialResponse] on the result of the activity
+ * invoked by the [PendingIntent]
+ */
+@RequiresApi(34)
+class PendingIntentHandler {
+ companion object {
+ private const val TAG = "PendingIntentHandler"
+
+ /**
+ * Extracts the [ProviderCreateCredentialRequest] from the provider's
+ * [PendingIntent] invoked by the Android system.
+ *
+ * @param intent the intent associated with the [Activity] invoked through the
+ * [PendingIntent]
+ *
+ * @throws NullPointerException If [intent] is null
+ */
+ @JvmStatic
+ fun retrieveProviderCreateCredentialRequest(intent: Intent):
+ ProviderCreateCredentialRequest? {
+ val frameworkReq: CreateCredentialRequest? =
+ intent.getParcelableExtra(
+ CredentialProviderService
+ .EXTRA_CREATE_CREDENTIAL_REQUEST, CreateCredentialRequest::class.java
+ )
+ if (frameworkReq == null) {
+ Log.i(TAG, "Request not found in pendingIntent")
+ return frameworkReq
+ }
+ return ProviderCreateCredentialRequest(
+ androidx.credentials.CreateCredentialRequest
+ .createFrom(
+ frameworkReq.type,
+ frameworkReq.data,
+ frameworkReq.data,
+ requireSystemProvider = false,
+ frameworkReq.callingAppInfo.origin
+ ) ?: return null,
+ frameworkReq.callingAppInfo
+ )
+ }
+
+ /**
+ * Extracts the [BeginGetCredentialRequest] from the provider's
+ * [PendingIntent] invoked by the Android system when the user
+ * selects an [AuthenticationAction].
+ *
+ * @param intent the intent associated with the [Activity] invoked through the
+ * [PendingIntent]
+ *
+ * @throws NullPointerException If [intent] is null
+ */
+ @JvmStatic
+ fun retrieveBeginGetCredentialRequest(intent: Intent): BeginGetCredentialRequest? {
+ val request = intent.getParcelableExtra(
+ "android.service.credentials.extra.BEGIN_GET_CREDENTIAL_REQUEST",
+ android.service.credentials.BeginGetCredentialRequest::class.java
+ )
+ return request?.let { BeginGetCredentialUtil.convertToJetpackRequest(it) }
+ }
+
+ /**
+ * Sets the [CreateCredentialResponse] on the result of the
+ * activity invoked by the [PendingIntent] set on a
+ * [CreateEntry].
+ *
+ * @param intent the intent to be set on the result of the [Activity] invoked through the
+ * [PendingIntent]
+ * @param response the response to be set as an extra on the [intent]
+ *
+ * @throws NullPointerException If [intent], or [response] is null
+ */
+ @JvmStatic
+ fun setCreateCredentialResponse(
+ intent: Intent,
+ response: CreateCredentialResponse
+ ) {
+ intent.putExtra(
+ CredentialProviderService.EXTRA_CREATE_CREDENTIAL_RESPONSE,
+ android.credentials.CreateCredentialResponse(response.data)
+ )
+ }
+
+ /**
+ * Extracts the [ProviderGetCredentialRequest] from the provider's
+ * [PendingIntent] invoked by the Android system, when the user selects a
+ * [CredentialEntry].
+ *
+ * @param intent the intent associated with the [Activity] invoked through the
+ * [PendingIntent]
+ *
+ * @throws NullPointerException If [intent] is null
+ */
+ @JvmStatic
+ fun retrieveProviderGetCredentialRequest(intent: Intent):
+ ProviderGetCredentialRequest? {
+ val frameworkReq = intent.getParcelableExtra(
+ CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST,
+ android.service.credentials.GetCredentialRequest::class.java
+ )
+ if (frameworkReq == null) {
+ Log.i(TAG, "Get request from framework is null")
+ return null
+ }
+ return ProviderGetCredentialRequest.createFrom(frameworkReq)
+ }
+
+ /**
+ * Sets the [android.credentials.GetCredentialResponse] on the result of the
+ * activity invoked by the [PendingIntent], set on a [CreateEntry].
+ *
+ * @param intent the intent to be set on the result of the [Activity] invoked through the
+ * [PendingIntent]
+ * @param response the response to be set as an extra on the [intent]
+ *
+ * @throws NullPointerException If [intent], or [response] is null
+ */
+ @JvmStatic
+ fun setGetCredentialResponse(
+ intent: Intent,
+ response: GetCredentialResponse
+ ) {
+ intent.putExtra(
+ CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE,
+ android.credentials.GetCredentialResponse(
+ android.credentials.Credential(
+ response.credential.type,
+ response.credential.data
+ )
+ )
+ )
+ }
+
+ /**
+ * Sets the [android.service.credentials.BeginGetCredentialResponse] on the result of the
+ * activity invoked by the [PendingIntent], set on an [AuthenticationAction].
+ *
+ * @param intent the intent to be set on the result of the [Activity] invoked through the
+ * [PendingIntent]
+ * @param response the response to be set as an extra on the [intent]
+ *
+ * @throws NullPointerException If [intent], or [response] is null
+ */
+ @JvmStatic
+ fun setBeginGetCredentialResponse(
+ intent: Intent,
+ response: BeginGetCredentialResponse
+ ) {
+ intent.putExtra(
+ CredentialProviderService.EXTRA_BEGIN_GET_CREDENTIAL_RESPONSE,
+ BeginGetCredentialUtil.convertToFrameworkResponse(response)
+ )
+ }
+
+ /**
+ * Sets the [androidx.credentials.exceptions.GetCredentialException] if an error is
+ * encountered during the final phase of the get credential flow.
+ *
+ * A credential provider service returns a list of [CredentialEntry] as part of
+ * the [BeginGetCredentialResponse] to the query phase of the get-credential flow.
+ * If the user selects one of these entries, the corresponding [PendingIntent]
+ * is fired and the provider's activity is invoked.
+ * If there is an error encountered during the lifetime of that activity, the provider
+ * must use this API to set an exception before finishing this activity.
+ *
+ * @param intent the intent to be set on the result of the [Activity] invoked through the
+ * [PendingIntent]
+ * @param exception the exception to be set as an extra to the [intent]
+ *
+ * @throws NullPointerException If [intent], or [exception] is null
+ */
+ @JvmStatic
+ fun setGetCredentialException(
+ intent: Intent,
+ exception: GetCredentialException
+ ) {
+ intent.putExtra(
+ CredentialProviderService.EXTRA_GET_CREDENTIAL_EXCEPTION,
+ android.credentials.GetCredentialException(exception.type, exception.message)
+ )
+ }
+
+ /**
+ * Sets the [androidx.credentials.exceptions.CreateCredentialException] if an error is
+ * encountered during the final phase of the create credential flow.
+ *
+ * A credential provider service returns a list of [CreateEntry] as part of
+ * the [BeginCreateCredentialResponse] to the query phase of the get-credential flow.
+ *
+ * If the user selects one of these entries, the corresponding [PendingIntent]
+ * is fired and the provider's activity is invoked. If there is an error encountered
+ * during the lifetime of that activity, the provider must use this API to set
+ * an exception before finishing the activity.
+ *
+ * @param intent the intent to be set on the result of the [Activity] invoked through the
+ * [PendingIntent]
+ * @param exception the exception to be set as an extra to the [intent]
+ *
+ * @throws NullPointerException If [intent], or [exception] is null
+ */
+ @JvmStatic
+ fun setCreateCredentialException(
+ intent: Intent,
+ exception: CreateCredentialException
+ ) {
+ intent.putExtra(
+ CredentialProviderService.EXTRA_CREATE_CREDENTIAL_EXCEPTION,
+ android.credentials.CreateCredentialException(exception.type, exception.message)
+ )
+ }
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderClearCredentialStateRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderClearCredentialStateRequest.kt
new file mode 100644
index 0000000..ff8d7eb
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderClearCredentialStateRequest.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.credentials.provider
+
+import android.service.credentials.CallingAppInfo
+
+/**
+ * Request class for clearing a user's credential state from the credential providers.
+ *
+ * @property callingAppInfo info pertaining to the calling app that's making the request
+ */
+class ProviderClearCredentialStateRequest constructor(val callingAppInfo: CallingAppInfo)
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt
new file mode 100644
index 0000000..b5557ef
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.credentials.provider
+
+import android.service.credentials.CallingAppInfo
+import androidx.credentials.CreateCredentialRequest
+
+/**
+ * Final request received by the provider after the user has selected a given [CreateEntry]
+ * on the UI.
+ *
+ * This request contains the actual request coming from the calling app,
+ * and the application information associated with the calling app.
+ *
+ * @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
+ *
+ * @throws NullPointerException If [callingRequest] is null
+ * @throws NullPointerException If [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
+)
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt
new file mode 100644
index 0000000..e91c671
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.credentials.provider
+
+import android.app.PendingIntent
+import android.service.credentials.CallingAppInfo
+import androidx.annotation.RequiresApi
+import androidx.credentials.CredentialOption
+import java.util.stream.Collectors
+
+/**
+ * Request received by the provider after the query phase of the get flow is complete i.e. the user
+ * was presented with a list of credentials, and the user has now made a selection from the list of
+ * [CredentialEntry] presented on the selector UI.
+ *
+ * This request will be added to the intent extras of the activity invoked by the [PendingIntent]
+ * set on the [CredentialEntry] that the user selected. The request
+ * must be extracted using the [PendingIntentHandler.retrieveProviderGetCredentialRequest] helper
+ * API.
+ *
+ * @property credentialOptions the list of credential retrieval options containing the
+ * required parameters.
+ * This list is 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]. This is because these
+ * entries are created for a given [BeginGetPasswordOption] or a [BeginGetPublicKeyCredentialOption]
+ * respectively, which corresponds to a single [CredentialOption].
+ *
+ * This list is 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]. This is because when a remote entry is selected. the entire
+ * request, containing multiple options, is sent to a remote device.
+ *
+ * @property callingAppInfo information pertaining to the calling application
+ *
+ * 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.
+ */
+@RequiresApi(34)
+class ProviderGetCredentialRequest constructor(
+ val credentialOptions: List<CredentialOption>,
+ val callingAppInfo: CallingAppInfo
+) {
+
+ /** @hide */
+ companion object {
+ internal fun createFrom(request: android.service.credentials.GetCredentialRequest):
+ ProviderGetCredentialRequest {
+ return ProviderGetCredentialRequest(
+ request.credentialOptions.stream()
+ .map { option ->
+ CredentialOption.createFrom(
+ option.type,
+ option.credentialRetrievalData,
+ option.candidateQueryData,
+ option.isSystemProviderRequired,
+ option.allowedProviders,
+ )
+ }
+ .collect(Collectors.toList()),
+ request.callingAppInfo)
+ }
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt
new file mode 100644
index 0000000..a4425c2
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt
@@ -0,0 +1,394 @@
+/*
+ * 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.credentials.provider
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.app.slice.Slice
+import android.app.slice.SliceSpec
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.credentials.CredentialOption
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.R
+import java.time.Instant
+import java.util.Collections
+
+/**
+ * A public key credential entry that is displayed on the account selector UI. This
+ * entry denotes that a credential of type [PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL]
+ * is available for the user to select.
+ *
+ * Once this entry is selected, the corresponding [pendingIntent] will be invoked. The provider
+ * can then show any activity they wish to. Before finishing the activity, provider must
+ * set the final [androidx.credentials.GetCredentialResponse] through the
+ * [PendingIntentHandler.setGetCredentialResponse] helper API.
+ *
+ * @property username the username of the account holding the public key credential
+ * @property displayName the displayName of the account holding the public key credential
+ * @property lastUsedTime the last used time of this entry
+ * @property 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 pendingIntent the [PendingIntent] to be invoked when the user
+ * selects this entry
+ * @property isAutoSelectAllowed whether this entry is allowed to be auto
+ * selected if it is the only one on the UI. Note that setting this value
+ * to true does not guarantee this behavior. The developer must also set this
+ * to true, and the framework must determine that it is safe to auto select.
+ *
+ * @throws IllegalArgumentException if [username] is empty
+ */
+@RequiresApi(28)
+class PublicKeyCredentialEntry internal constructor(
+ val username: CharSequence,
+ val displayName: CharSequence?,
+ val typeDisplayName: CharSequence,
+ val pendingIntent: PendingIntent,
+ val icon: Icon,
+ val lastUsedTime: Instant?,
+ val isAutoSelectAllowed: Boolean,
+ beginGetPublicKeyCredentialOption: BeginGetPublicKeyCredentialOption,
+ /** @hide */
+ val autoSelectAllowedFromOption: Boolean = false,
+ /** @hide */
+ val isDefaultIcon: Boolean = false
+) : CredentialEntry(
+ PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
+ beginGetPublicKeyCredentialOption,
+ toSlice(
+ PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
+ username,
+ displayName,
+ pendingIntent,
+ typeDisplayName,
+ lastUsedTime,
+ icon,
+ isAutoSelectAllowed,
+ beginGetPublicKeyCredentialOption
+ )
+) {
+
+ init {
+ require(username.isNotEmpty()) { "username must not be empty" }
+ require(typeDisplayName.isNotEmpty()) { "typeDisplayName must not be empty" }
+ }
+
+ 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,
+ ) : this(
+ username,
+ displayName,
+ context.getString(
+ R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL
+ ),
+ pendingIntent,
+ icon,
+ lastUsedTime,
+ isAutoSelectAllowed,
+ beginGetPublicKeyCredentialOption
+ )
+
+ /** @hide **/
+ @Suppress("AcronymName")
+ companion object {
+ private const val TAG = "PublicKeyCredEntry"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_TYPE_DISPLAY_NAME =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_TITLE =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_USER_NAME"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_SUBTITLE =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_CREDENTIAL_TYPE_DISPLAY_NAME"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_LAST_USED_TIME_MILLIS =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_ICON =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_PROFILE_ICON"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_PENDING_INTENT =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_PENDING_INTENT"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_AUTO_ALLOWED =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_ALLOWED"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_OPTION_ID =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_OPTION_ID"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_AUTO_SELECT_FROM_OPTION =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_SELECT_FROM_OPTION"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_DEFAULT_ICON_RES_ID =
+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEFAULT_ICON_RES_ID"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val AUTO_SELECT_TRUE_STRING = "true"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val AUTO_SELECT_FALSE_STRING = "false"
+
+ /** @hide */
+ @RequiresApi(28)
+ @JvmStatic
+ fun toSlice(
+ type: String,
+ title: CharSequence,
+ subTitle: CharSequence?,
+ pendingIntent: PendingIntent,
+ typeDisplayName: CharSequence?,
+ lastUsedTime: Instant?,
+ icon: Icon,
+ isAutoSelectAllowed: Boolean,
+ beginGetPublicKeyCredentialOption: BeginGetPublicKeyCredentialOption
+ ): Slice {
+ // TODO("Put the right revision value")
+ val autoSelectAllowed = if (isAutoSelectAllowed) {
+ AUTO_SELECT_TRUE_STRING
+ } else {
+ AUTO_SELECT_FALSE_STRING
+ }
+ val sliceBuilder = Slice.Builder(
+ Uri.EMPTY, SliceSpec(
+ type, 1
+ )
+ )
+ .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)
+ )
+ try {
+ if (icon.resId == R.drawable.ic_passkey) {
+ sliceBuilder.addInt(
+ /*true=*/1,
+ /*subType=*/null,
+ listOf(SLICE_HINT_DEFAULT_ICON_RES_ID)
+ )
+ }
+ } catch (_: IllegalStateException) {
+ }
+
+ if (CredentialOption.extractAutoSelectValue(
+ beginGetPublicKeyCredentialOption.candidateQueryData
+ )
+ ) {
+ sliceBuilder.addInt(
+ /*true=*/1,
+ /*subType=*/null,
+ listOf(SLICE_HINT_AUTO_SELECT_FROM_OPTION)
+ )
+ }
+ if (lastUsedTime != null) {
+ sliceBuilder.addLong(
+ lastUsedTime.toEpochMilli(),
+ /*subType=*/null,
+ listOf(SLICE_HINT_LAST_USED_TIME_MILLIS)
+ )
+ }
+ sliceBuilder.addAction(
+ pendingIntent,
+ Slice.Builder(sliceBuilder)
+ .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
+ .build(),
+ /*subType=*/null
+ )
+ return sliceBuilder.build()
+ }
+
+ /**
+ * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
+ *
+ * @param slice the [Slice] object constructed through [toSlice]
+ *
+ * @hide
+ */
+ @RequiresApi(28)
+ @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+ @JvmStatic
+ fun fromSlice(slice: Slice): PublicKeyCredentialEntry? {
+ var typeDisplayName: CharSequence? = null
+ var title: CharSequence? = null
+ var subTitle: CharSequence? = null
+ var icon: Icon? = null
+ var pendingIntent: PendingIntent? = null
+ var lastUsedTime: Instant? = null
+ var autoSelectAllowed = false
+ var beginGetPublicKeyCredentialOptionId: CharSequence? = null
+ var autoSelectAllowedFromOption = false
+ var isDefaultIcon = false
+
+ slice.items.forEach {
+ 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
+ } 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)) {
+ 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)) {
+ val autoSelectValue = it.text
+ if (autoSelectValue == AUTO_SELECT_TRUE_STRING) {
+ autoSelectAllowed = true
+ }
+ } else if (it.hasHint(SLICE_HINT_AUTO_SELECT_FROM_OPTION)) {
+ autoSelectAllowedFromOption = true
+ } else if (it.hasHint(SLICE_HINT_DEFAULT_ICON_RES_ID)) {
+ isDefaultIcon = true
+ }
+ }
+
+ return try {
+ PublicKeyCredentialEntry(
+ title!!,
+ subTitle,
+ typeDisplayName!!,
+ pendingIntent!!,
+ icon!!,
+ lastUsedTime,
+ autoSelectAllowed,
+ BeginGetPublicKeyCredentialOption.createFromEntrySlice(
+ Bundle(),
+ beginGetPublicKeyCredentialOptionId!!.toString()
+ ),
+ autoSelectAllowedFromOption,
+ isDefaultIcon
+ )
+ } catch (e: Exception) {
+ Log.i(TAG, "fromSlice failed with: " + e.message)
+ null
+ }
+ }
+ }
+
+ /**
+ * Builder for [PublicKeyCredentialEntry]
+ */
+ class Builder(
+ private val context: Context,
+ private val username: CharSequence,
+ private val pendingIntent: PendingIntent,
+ private val beginGetPublicKeyCredentialOption: BeginGetPublicKeyCredentialOption
+ ) {
+ private var displayName: CharSequence? = null
+ private var lastUsedTime: Instant? = null
+ private var icon: Icon? = null
+ private var autoSelectAllowed: Boolean = false
+
+ /** Sets a displayName to be shown on the UI with this entry */
+ fun setDisplayName(displayName: CharSequence?): Builder {
+ this.displayName = displayName
+ return this
+ }
+
+ /** Sets the icon to be shown on the UI with this entry */
+ fun setIcon(icon: Icon): Builder {
+ this.icon = icon
+ return this
+ }
+
+ /**
+ * Sets whether the entry should be auto-selected.
+ * The value is false by default
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ fun setAutoSelectAllowed(autoSelectAllowed: Boolean): Builder {
+ this.autoSelectAllowed = autoSelectAllowed
+ return this
+ }
+
+ /**
+ * Sets the last used time of this account
+ *
+ * This information will be used to sort the entries on the selector.
+ */
+ fun setLastUsedTime(lastUsedTime: Instant?): Builder {
+ this.lastUsedTime = lastUsedTime
+ return this
+ }
+
+ /** Builds an instance of [PublicKeyCredentialEntry] */
+ fun build(): PublicKeyCredentialEntry {
+ if (icon == null) {
+ icon = Icon.createWithResource(context, R.drawable.ic_passkey)
+ }
+ val typeDisplayName = context.getString(
+ R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL
+ )
+ return PublicKeyCredentialEntry(
+ username,
+ displayName,
+ typeDisplayName,
+ pendingIntent,
+ icon!!,
+ lastUsedTime,
+ autoSelectAllowed,
+ beginGetPublicKeyCredentialOption
+ )
+ }
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt
new file mode 100644
index 0000000..ba2831a
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.credentials.provider
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.app.slice.Slice
+import android.app.slice.SliceSpec
+import android.net.Uri
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import java.util.Collections
+
+/**
+ * An entry on the selector, denoting that the credential request will be completed on a remote
+ * device.
+ *
+ * Once this entry is selected, the corresponding [pendingIntent] will be invoked. The provider
+ * can then show any activity they wish to. Before finishing the activity, provider must
+ * set the final [androidx.credentials.GetCredentialResponse] through the
+ * [PendingIntentHandler.setGetCredentialResponse] helper API, or a
+ * [androidx.credentials.CreateCredentialResponse] through the
+ * [PendingIntentHandler.setCreateCredentialResponse] helper API depending on whether it is a get
+ * or create flow.
+ *
+ * @property pendingIntent the [PendingIntent] to be invoked when the user selects
+ * this entry
+ *
+ * See [android.service.credentials.BeginGetCredentialResponse] for usage details.
+ */
+class RemoteEntry constructor(
+ val pendingIntent: PendingIntent
+) {
+
+ /** @hide **/
+ @Suppress("AcronymName")
+ companion object {
+ private const val TAG = "RemoteEntry"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal const val SLICE_HINT_PENDING_INTENT =
+ "androidx.credentials.provider.remoteEntry.SLICE_HINT_PENDING_INTENT"
+
+ /** @hide */
+ @RequiresApi(28)
+ @JvmStatic
+ fun toSlice(
+ remoteEntry: RemoteEntry
+ ): Slice {
+ val pendingIntent = remoteEntry.pendingIntent
+ // TODO("Put the right spec and version value")
+ val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))
+ sliceBuilder.addAction(
+ pendingIntent,
+ Slice.Builder(sliceBuilder)
+ .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
+ .build(), /*subType=*/null
+ )
+ return sliceBuilder.build()
+ }
+
+ /**
+ * Returns an instance of [RemoteEntry] derived from a [Slice] object.
+ *
+ * @param slice the [Slice] object constructed through [toSlice]
+ *
+ * @hide
+ */
+ @RequiresApi(28)
+ @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+ @JvmStatic
+ fun fromSlice(slice: Slice): RemoteEntry? {
+ var pendingIntent: PendingIntent? = null
+ slice.items.forEach {
+ if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
+ pendingIntent = it.action
+ }
+ }
+ return try {
+ RemoteEntry(pendingIntent!!)
+ } catch (e: Exception) {
+ Log.i(TAG, "fromSlice failed with: " + e.message)
+ null
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginCreateCredentialUtil.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginCreateCredentialUtil.kt
new file mode 100644
index 0000000..1b03068
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginCreateCredentialUtil.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.credentials.provider.utils
+
+import android.annotation.SuppressLint
+import androidx.credentials.provider.BeginCreateCredentialRequest
+import androidx.annotation.RequiresApi
+import androidx.credentials.PasswordCredential
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.internal.FrameworkClassParsingException
+import androidx.credentials.provider.BeginCreateCredentialResponse
+import androidx.credentials.provider.BeginCreateCustomCredentialRequest
+import androidx.credentials.provider.BeginCreatePasswordCredentialRequest
+import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
+import androidx.credentials.provider.CreateEntry
+import androidx.credentials.provider.RemoteEntry
+import java.util.stream.Collectors
+
+/**
+ * @hide
+ */
+@RequiresApi(34)
+class BeginCreateCredentialUtil {
+ companion object {
+ @JvmStatic
+ internal fun convertToJetpackRequest(
+ request: android.service.credentials.BeginCreateCredentialRequest
+ ):
+ BeginCreateCredentialRequest {
+ return try {
+ when (request.type) {
+ PasswordCredential.TYPE_PASSWORD_CREDENTIAL -> {
+ BeginCreatePasswordCredentialRequest.createFrom(
+ request.data, request.callingAppInfo
+ )
+ }
+
+ PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL -> {
+ BeginCreatePublicKeyCredentialRequest.createFrom(
+ request.data, request.callingAppInfo
+ )
+ }
+
+ else -> {
+ BeginCreateCustomCredentialRequest(
+ request.type, request.data,
+ request.callingAppInfo
+ )
+ }
+ }
+ } catch (e: FrameworkClassParsingException) {
+ BeginCreateCustomCredentialRequest(
+ request.type,
+ request.data,
+ request.callingAppInfo
+ )
+ }
+ }
+
+ fun convertToFrameworkResponse(
+ response: BeginCreateCredentialResponse
+ ): android.service.credentials.BeginCreateCredentialResponse {
+ val frameworkBuilder = android.service.credentials.BeginCreateCredentialResponse
+ .Builder()
+ populateCreateEntries(frameworkBuilder, response.createEntries)
+ populateRemoteEntry(frameworkBuilder, response.remoteEntry)
+ return frameworkBuilder.build()
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun populateRemoteEntry(
+ frameworkBuilder: android.service.credentials.BeginCreateCredentialResponse.Builder,
+ remoteEntry: RemoteEntry?
+ ) {
+ if (remoteEntry == null) {
+ return
+ }
+ frameworkBuilder.setRemoteCreateEntry(
+ android.service.credentials.RemoteEntry(
+ RemoteEntry.toSlice(remoteEntry)
+ )
+ )
+ }
+
+ private fun populateCreateEntries(
+ frameworkBuilder: android.service.credentials.BeginCreateCredentialResponse.Builder,
+ createEntries: List<CreateEntry>
+ ) {
+ createEntries.forEach {
+ frameworkBuilder.addCreateEntry(
+ android.service.credentials.CreateEntry(
+ CreateEntry.toSlice(it)
+ )
+ )
+ }
+ }
+
+ fun convertToFrameworkRequest(request: BeginCreateCredentialRequest):
+ android.service.credentials.BeginCreateCredentialRequest {
+ return android.service.credentials.BeginCreateCredentialRequest(request.type,
+ request.candidateQueryData, request.callingAppInfo)
+ }
+
+ fun convertToJetpackResponse(
+ frameworkResponse: android.service.credentials.BeginCreateCredentialResponse
+ ): BeginCreateCredentialResponse {
+ return BeginCreateCredentialResponse(
+ createEntries = frameworkResponse.createEntries.stream()
+ .map { entry -> CreateEntry.fromSlice(entry.slice) }
+ .filter { entry -> entry != null }
+ .map { entry -> entry!! }
+ .collect(Collectors.toList()),
+ remoteEntry =
+ frameworkResponse.remoteCreateEntry?.let { RemoteEntry.fromSlice(it.slice) }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginGetCredentialUtil.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginGetCredentialUtil.kt
new file mode 100644
index 0000000..8f6bfd5
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/BeginGetCredentialUtil.kt
@@ -0,0 +1,162 @@
+/*
+ * 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.credentials.provider.utils
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import androidx.credentials.provider.BeginGetCredentialOption
+import androidx.credentials.provider.BeginGetCredentialRequest
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.credentials.provider.Action
+import androidx.credentials.provider.AuthenticationAction
+import androidx.credentials.provider.BeginGetCredentialResponse
+import androidx.credentials.provider.CredentialEntry
+import androidx.credentials.provider.RemoteEntry
+import java.util.stream.Collectors
+
+@RequiresApi(34)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class BeginGetCredentialUtil {
+ companion object {
+ @JvmStatic
+ internal fun convertToJetpackRequest(
+ request: android.service.credentials.BeginGetCredentialRequest
+ ): BeginGetCredentialRequest {
+ val beginGetCredentialOptions: MutableList<BeginGetCredentialOption> =
+ mutableListOf()
+ request.beginGetCredentialOptions.forEach {
+ beginGetCredentialOptions.add(
+ BeginGetCredentialOption.createFrom(
+ it.id, it.type, it.candidateQueryData
+ )
+ )
+ }
+ return BeginGetCredentialRequest(
+ callingAppInfo = request.callingAppInfo,
+ beginGetCredentialOptions = beginGetCredentialOptions
+ )
+ }
+
+ fun convertToFrameworkResponse(response: BeginGetCredentialResponse):
+ android.service.credentials.BeginGetCredentialResponse {
+ val frameworkBuilder = android.service.credentials.BeginGetCredentialResponse.Builder()
+ populateCredentialEntries(frameworkBuilder, response.credentialEntries)
+ populateActionEntries(frameworkBuilder, response.actions)
+ populateAuthenticationEntries(frameworkBuilder, response.authenticationActions)
+ populateRemoteEntry(frameworkBuilder, response.remoteEntry)
+ return frameworkBuilder.build()
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun populateRemoteEntry(
+ frameworkBuilder: android.service.credentials.BeginGetCredentialResponse.Builder,
+ remoteEntry: RemoteEntry?
+ ) {
+ if (remoteEntry == null) {
+ return
+ }
+ frameworkBuilder.setRemoteCredentialEntry(
+ android.service.credentials.RemoteEntry(RemoteEntry.toSlice(remoteEntry))
+ )
+ }
+
+ private fun populateAuthenticationEntries(
+ frameworkBuilder: android.service.credentials.BeginGetCredentialResponse.Builder,
+ authenticationActions: List<AuthenticationAction>
+ ) {
+ authenticationActions.forEach {
+ frameworkBuilder.addAuthenticationAction(
+ android.service.credentials.Action(
+ AuthenticationAction.toSlice(it)
+ )
+ )
+ }
+ }
+
+ private fun populateActionEntries(
+ builder: android.service.credentials.BeginGetCredentialResponse.Builder,
+ actionEntries: List<Action>
+ ) {
+ actionEntries.forEach {
+ builder.addAction(
+ android.service.credentials.Action(
+ Action.toSlice(it)
+ )
+ )
+ }
+ }
+
+ private fun populateCredentialEntries(
+ builder: android.service.credentials.BeginGetCredentialResponse.Builder,
+ credentialEntries: List<CredentialEntry>
+ ) {
+ credentialEntries.forEach {
+ builder.addCredentialEntry(
+ android.service.credentials.CredentialEntry(
+ android.service.credentials.BeginGetCredentialOption(
+ it.beginGetCredentialOption.id,
+ it.type,
+ Bundle.EMPTY
+ ),
+ it.slice
+ )
+ )
+ }
+ }
+
+ fun convertToFrameworkRequest(request: BeginGetCredentialRequest):
+ android.service.credentials.BeginGetCredentialRequest {
+ return android.service.credentials.BeginGetCredentialRequest.Builder()
+ .setCallingAppInfo(request.callingAppInfo)
+ .setBeginGetCredentialOptions(request.beginGetCredentialOptions.stream()
+ .map { option -> convertToJetpackBeginOption(option) }
+ .collect(Collectors.toList()))
+ .build()
+ }
+
+ private fun convertToJetpackBeginOption(option: BeginGetCredentialOption):
+ android.service.credentials.BeginGetCredentialOption {
+ return android.service.credentials.BeginGetCredentialOption(option.id, option.type,
+ option.candidateQueryData)
+ }
+
+ fun convertToJetpackResponse(
+ response: android.service.credentials.BeginGetCredentialResponse
+ ): BeginGetCredentialResponse {
+ return BeginGetCredentialResponse(
+ credentialEntries = response.credentialEntries.stream()
+ .map { entry -> CredentialEntry.createFrom(entry.slice) }
+ .filter { entry -> entry != null }
+ .map { entry -> entry!! }
+ .collect(Collectors.toList()),
+ actions = response.actions.stream()
+ .map { entry -> Action.fromSlice(entry.slice) }
+ .filter { entry -> entry != null }
+ .map { entry -> entry!! }
+ .collect(Collectors.toList()),
+ authenticationActions = response.authenticationActions.stream()
+ .map { entry -> AuthenticationAction.fromSlice(entry.slice) }
+ .filter { entry -> entry != null }
+ .map { entry -> entry!! }
+ .collect(Collectors.toList()),
+ remoteEntry =
+ response.remoteCredentialEntry?.let { RemoteEntry.fromSlice(it.slice) }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/utils/ClearCredentialUtil.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/ClearCredentialUtil.kt
new file mode 100644
index 0000000..460a368
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/ClearCredentialUtil.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.credentials.provider.utils
+
+import android.service.credentials.ClearCredentialStateRequest
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.credentials.provider.ProviderClearCredentialStateRequest
+
+@RequiresApi(34)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class ClearCredentialUtil {
+ companion object {
+ @JvmStatic
+ internal fun convertToJetpackRequest(request: ClearCredentialStateRequest):
+ ProviderClearCredentialStateRequest {
+ return ProviderClearCredentialStateRequest(request.callingAppInfo)
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/res/values-ky/strings.xml b/credentials/credentials/src/main/res/values-ky/strings.xml
index 3366129..240c775 100644
--- a/credentials/credentials/src/main/res/values-ky/strings.xml
+++ b/credentials/credentials/src/main/res/values-ky/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-ta/strings.xml b/credentials/credentials/src/main/res/values-ta/strings.xml
index 458bcb4..d3f9b1f 100644
--- a/credentials/credentials/src/main/res/values-ta/strings.xml
+++ b/credentials/credentials/src/main/res/values-ta/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/cursoradapter/cursoradapter/build.gradle b/cursoradapter/cursoradapter/build.gradle
index 486beb8..05db38f 100644
--- a/cursoradapter/cursoradapter/build.gradle
+++ b/cursoradapter/cursoradapter/build.gradle
@@ -10,7 +10,7 @@
}
androidx {
- name = "Android Support Library Cursor Adapter"
+ name = "Cursor Adapter"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/customview/customview-poolingcontainer/build.gradle b/customview/customview-poolingcontainer/build.gradle
index 49ae408..524083d 100644
--- a/customview/customview-poolingcontainer/build.gradle
+++ b/customview/customview-poolingcontainer/build.gradle
@@ -36,7 +36,7 @@
}
androidx {
- name = "androidx.customview:poolingcontainer"
+ name = "CustomView Pooling Container"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.CUSTOMVIEW_POOLINGCONTAINER
inceptionYear = "2021"
diff --git a/customview/customview/build.gradle b/customview/customview/build.gradle
index 041da7f..e184a65 100644
--- a/customview/customview/build.gradle
+++ b/customview/customview/build.gradle
@@ -20,7 +20,7 @@
}
androidx {
- name = "Android Support Library Custom View"
+ name = "Custom View"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.CUSTOMVIEW
inceptionYear = "2018"
diff --git a/datastore/datastore-core-okio/build.gradle b/datastore/datastore-core-okio/build.gradle
index d1c5ba1..99bb8ec 100644
--- a/datastore/datastore-core-okio/build.gradle
+++ b/datastore/datastore-core-okio/build.gradle
@@ -97,7 +97,7 @@
}
androidx {
- name = "Android DataStore Core Okio"
+ name = "DataStore Core Okio"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2020"
description = "Android DataStore Core Okio- contains APIs to use datastore-core in multiplatform via okio"
diff --git a/datastore/datastore-core/build.gradle b/datastore/datastore-core/build.gradle
index da45ae9..67e6e17 100644
--- a/datastore/datastore-core/build.gradle
+++ b/datastore/datastore-core/build.gradle
@@ -156,7 +156,7 @@
}
androidx {
- name = "Android DataStore Core"
+ name = "DataStore Core"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2020"
description = "Android DataStore Core - contains the underlying store used by each serialization method"
diff --git a/datastore/datastore-core/src/jvmTest/kotlin/androidx/datastore/core/SimpleActorTest.kt b/datastore/datastore-core/src/jvmTest/kotlin/androidx/datastore/core/SimpleActorTest.kt
index e6446b5..154e868 100644
--- a/datastore/datastore-core/src/jvmTest/kotlin/androidx/datastore/core/SimpleActorTest.kt
+++ b/datastore/datastore-core/src/jvmTest/kotlin/androidx/datastore/core/SimpleActorTest.kt
@@ -73,6 +73,7 @@
assertThat(msgs).isEqualTo(listOf(1, 2, 3, 4))
}
+ @Ignore("b/281516026")
@Test
fun testOnCompleteIsCalledWhenScopeIsCancelled() = runBlocking<Unit> {
val scope = CoroutineScope(Job())
diff --git a/datastore/datastore-preferences-core/build.gradle b/datastore/datastore-preferences-core/build.gradle
index a9112b0..9567f6b 100644
--- a/datastore/datastore-preferences-core/build.gradle
+++ b/datastore/datastore-preferences-core/build.gradle
@@ -119,7 +119,7 @@
}
androidx {
- name = "Android Preferences DataStore Core"
+ name = "Preferences DataStore Core"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2020"
description = "Android Preferences DataStore without the Android Dependencies"
diff --git a/datastore/datastore-preferences-proto/build.gradle b/datastore/datastore-preferences-proto/build.gradle
index 258630b..f599a23 100644
--- a/datastore/datastore-preferences-proto/build.gradle
+++ b/datastore/datastore-preferences-proto/build.gradle
@@ -73,7 +73,7 @@
artifacts.add(jarjarConf.name, preferencesProtoJarJarTask.flatMap { it.archiveFile })
androidx {
- name = "Android Preferences DataStore Proto"
+ name = "Preferences DataStore Proto"
publish = Publish.NONE
inceptionYear = "2020"
description = "Jarjar the generated proto and proto-lite dependency for use by " +
diff --git a/datastore/datastore-preferences-rxjava2/build.gradle b/datastore/datastore-preferences-rxjava2/build.gradle
index d58618b..f80ea3d 100644
--- a/datastore/datastore-preferences-rxjava2/build.gradle
+++ b/datastore/datastore-preferences-rxjava2/build.gradle
@@ -56,7 +56,7 @@
}
androidx {
- name = "Android DataStore Core RxJava2 Wrappers"
+ name = "DataStore Preferences RxJava2"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2020"
description = "Android DataStore Core - contains wrappers for using DataStore using RxJava2"
diff --git a/datastore/datastore-preferences-rxjava3/build.gradle b/datastore/datastore-preferences-rxjava3/build.gradle
index 8461448..7a06c51 100644
--- a/datastore/datastore-preferences-rxjava3/build.gradle
+++ b/datastore/datastore-preferences-rxjava3/build.gradle
@@ -56,7 +56,7 @@
}
androidx {
- name = "Android DataStore Core RxJava2 Wrappers"
+ name = "DataStore Preferences RxJava3"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2020"
description = "Android DataStore Core - contains wrappers for using DataStore using RxJava2"
diff --git a/datastore/datastore-preferences/build.gradle b/datastore/datastore-preferences/build.gradle
index 37dfd741e..0bbbbe6 100644
--- a/datastore/datastore-preferences/build.gradle
+++ b/datastore/datastore-preferences/build.gradle
@@ -77,7 +77,7 @@
androidx {
- name = "Android Preferences DataStore"
+ name = "Preferences DataStore"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2020"
description = "Android Preferences DataStore"
diff --git a/datastore/datastore-proto/build.gradle b/datastore/datastore-proto/build.gradle
index a3680b0..658823c 100644
--- a/datastore/datastore-proto/build.gradle
+++ b/datastore/datastore-proto/build.gradle
@@ -52,7 +52,7 @@
}
androidx {
- name = "Android Proto DataStore"
+ name = "Proto DataStore"
publish = Publish.NONE
inceptionYear = "2020"
description = "Android Proto DataStore"
diff --git a/datastore/datastore-rxjava2/build.gradle b/datastore/datastore-rxjava2/build.gradle
index 20edf8fb..f2354c5 100644
--- a/datastore/datastore-rxjava2/build.gradle
+++ b/datastore/datastore-rxjava2/build.gradle
@@ -52,7 +52,7 @@
}
androidx {
- name = "Android DataStore Core RxJava2 Wrappers"
+ name = "DataStore RxJava2"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2020"
description = "Android DataStore Core - contains wrappers for using DataStore using RxJava2"
diff --git a/datastore/datastore-rxjava3/build.gradle b/datastore/datastore-rxjava3/build.gradle
index c9bfe85..8de1572 100644
--- a/datastore/datastore-rxjava3/build.gradle
+++ b/datastore/datastore-rxjava3/build.gradle
@@ -52,7 +52,7 @@
}
androidx {
- name = "Android DataStore Core RxJava2 Wrappers"
+ name = "DataStore RxJava3"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2020"
description = "Android DataStore Core - contains wrappers for using DataStore using RxJava2"
diff --git a/datastore/datastore/build.gradle b/datastore/datastore/build.gradle
index d450a9b..8b4451d 100644
--- a/datastore/datastore/build.gradle
+++ b/datastore/datastore/build.gradle
@@ -106,7 +106,7 @@
}
androidx {
- name = "Android DataStore"
+ name = "DataStore"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2020"
description = "Android DataStore - contains the underlying store used by each serialization " +
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 10836a5..5248e3c 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -1016,6 +1016,10 @@
WARNING:.*The option setting 'android\.r8\.maxWorkers=[0-9]+' is experimental\.
# Building XCFrameworks (b/260140834) and iOS benchmark invocation
.*xcodebuild.*
+Observed package id 'platforms;android-33-ext5' in inconsistent location.*
+.*xcodebuild.*
+# > Task :core:core:compileDebugAndroidTestKotlin
+w: file://\$SUPPORT/core/core/src/androidTest/java/androidx/core/util/TypedValueCompatTest\.kt:[0-9]+:[0-9]+ 'scaledDensity: Float' is deprecated\. Deprecated in Java
# > Task :wear:tiles:tiles-material:compileDebugJavaWithJavac
\$SUPPORT/wear/tiles/tiles\-material/src/main/java/androidx/wear/tiles/material/CircularProgressIndicator\.java:[0-9]+: warning: \[deprecation\] Helper in androidx\.wear\.tiles\.material has been deprecated
import static androidx\.wear\.tiles\.material\.Helper\.checkNotNull;
@@ -1045,4 +1049,307 @@
w: file://\$SUPPORT/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlePopupPositionTest\.kt:[0-9]+:[0-9]+ 'getter for windowLayoutParams: EspressoOptional<WindowManager\.LayoutParams!>!' is deprecated\. Deprecated in Java
w: file://\$SUPPORT/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldVisualTransformationMagnifierTest\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
# > Task :compose:ui:ui:compileDebugAndroidTestKotlin
-w: file://\$SUPPORT/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTestUtils\.kt:[0-9]+:[0-9]+ 'getter for windowLayoutParams: EspressoOptional<WindowManager\.LayoutParams!>!' is deprecated\. Deprecated in Java
\ No newline at end of file
+w: file://\$SUPPORT/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTestUtils\.kt:[0-9]+:[0-9]+ 'getter for windowLayoutParams: EspressoOptional<WindowManager\.LayoutParams!>!' is deprecated\. Deprecated in Java
+# > Task :compose:ui:ui-inspection:dexInspectorRelease
+Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.JvmFunctionSignature\$FakeJavaAnnotationConstructor\$asString\$[0-9]+'s kotlin\.Metadata: null
+Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$Method\$Instance's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.framework\.ViewExtensionsKt\$ancestors\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.Java[0-9]+RepeatableContainerLoader\$Cache's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.JvmPropertySignature's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.InspectorNode\$WhenMappings's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KotlinReflectionInternalError's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KDeclarationContainerImpl\$findFunctionDescriptor\$allMembers\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.RuntimeTypeMapperKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.AnnotationConstructorCallerKt\$createAnnotationInstance\$hashCode\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KMutableProperty[0-9]+Impl\$_setter\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.compose\.AndroidComposeViewWrapper\$Companion's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KParameterImpl\$type\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.KClasses\$isSubclassOf\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPackageImpl\$Data\$members\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.AnnotationConstructorCaller's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.UiToolingDataApi's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KCallableImpl\$_parameters\$[0-9]+\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.LambdaLocation's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KTypeImpl's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.ComposeLayoutInspector\$getComposableNodes\$data\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.ModuleByClassLoaderKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.JvmPropertySignature\$JavaField's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$supertypes\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ParameterFactory\$expand\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KProperty[0-9]+Impl\$Getter's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.AnnotationConstructorCaller\$CallMode's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KFunctionImpl\$caller\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$FieldSetter\$BoundInstance's kotlin\.Metadata: null
+Info:
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$Method\$Instance's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.ParameterInformation's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.IllegalPropertyDelegateAccessException's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.ComposeLayoutInspector's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.InlineClassAwareCaller's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.KClasses\$defaultType\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.PackageHashesKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KCallableImpl\$_returnType\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.JvmFunctionSignature\$FakeJavaAnnotationConstructor\$asString\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.InlineClassAwareCaller\$BoxUnboxData's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ParameterFactory\$loadConstantsFromStaticFinal\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KProperty[0-9]+Impl's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.ComposeLayoutInspector\$handleGetAllParametersCommand\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.ContextCache's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.RuntimeTypeMapperKt\$signature\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ParameterKind's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$BoundConstructor's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.ComposeLayoutInspector\$handleUnknownCommand\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.InlineClassAwareCallerKt's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ParameterFactory\$ParameterCreator\$findBestResourceFont\$\$inlined\$filterIsInstance\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.InlineClassConverter\$loadTypeMapper\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPropertyImpl\$Getter\$descriptor\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.KClasses's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KMutableProperty[0-9]+Impl\$Setter's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.CachesKt\$CACHE_FOR_BASE_CLASSIFIERS\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KCallableImpl\$_returnType\$[0-9]+\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KTypeParameterImpl's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KTypeImpl\$arguments\$[0-9]+\$parameterizedTypeArguments\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KMutableProperty[0-9]+Impl's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ParameterType's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$Method's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$Method\$JvmStaticInObject's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$FieldGetter\$BoundInstance's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.compose\.ComposeExtensionsKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.ComputableClassValue's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ParameterFactory\$loadConstantsFromObjectInstance\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.RecompositionHandler\$MethodKey's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPropertyImpl\$Setter\$descriptor\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KCallableImpl\$_annotations\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.LayoutInspectorTree\$belongsToView\$[0-9]+\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KTypeImpl\$arguments\$[0-9]+\$[0-9]+\$type\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$typeParameters\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ParameterFactory\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$FieldSetter\$BoundJvmStaticInObject's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.KTypesJvm's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.RecompositionHandler\$Data's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.CachesKt\$K_PACKAGE_CACHE\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$supertypes\$[0-9]+\$[0-9]+\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPropertyImpl's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KDeclarationContainerImpl\$MemberBelonginess's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.JvmPropertySignature\$JavaMethodProperty's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KFunctionImpl\$descriptor\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$FieldSetter\$Static's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KCallableImpl\$_parameters\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$FieldGetter\$BoundJvmStaticInObject's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$declaredNonStaticMembers\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$inheritedStaticMembers\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.NodeGroup's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.KCallables\$callSuspendBy\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ParameterFactory\$ParameterCreator\$lookup\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.proto\.ViewExtensionsKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPropertyImplKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.JvmPropertySignature\$KotlinProperty's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$FieldGetter\$JvmStaticInObject's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$FieldGetter's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$AccessorForHiddenConstructor's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$inheritedNonStaticMembers\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KProperty[0-9]+Impl\$delegateSource\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.SlotTreeKt\$extractParameterInfo\$\$inlined\$sortedBy\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$data\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.ComposeLayoutInspector\$handleGetComposablesCommand\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.WeakClassLoaderBox's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$constructors\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.AnnotationConstructorCallerKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KCallableImpl\$_parameters\$[0-9]+\$invoke\$\$inlined\$sortBy\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$FieldSetter\$JvmStaticInObject's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.ReflectionObjectRenderer\$renderLambda\$[0-9]+\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KParameterImpl\$annotations\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.KClassesJvm's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPropertyImpl\$Accessor's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.JvmFunctionSignature\$JavaMethod's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.LayoutInspectorTree\$belongsToView\$[0-9]+\$invoke\$\$inlined\$filterIsInstance\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.ReflectLambdaKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPropertyImpl\$Getter's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ScopedReflectionFactory's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.SlotTreeKt's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.ComposeLayoutInspector\$getComposableFromAnchor\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.ExperimentalReflectionOnLambdas's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.LayoutInspectorTree\$parseLayoutInfo\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.RuntimeTypeMapper's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.ComposeLayoutInspector\$WhenMappings's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.Parameter's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.util\.AnchorMap's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPackageImpl\$Data\$multifileFacade\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.AnnotationConstructorCallerKt\$createAnnotationInstance\$toString\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.AnnotationConstructorCallerKt\$createAnnotationInstance\$toString\$[0-9]+\$[0-9]+\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.LayoutInspectorTree\$belongsToView\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.RecompositionHandlerKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KParameterImpl's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KDeclarationContainerImpl's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ParameterFactory's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.ConcurrentHashMapCache's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.CacheByClassKt's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.InspectorNode's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.RecompositionHandler's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$annotations\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.ReflectLambdaKt\$reflect\$descriptor\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$Method\$BoundInstance's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KDeclarationContainerImpl\$Data\$moduleData\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.ComposeLayoutInspector\$handleGetParameterDetailsCommand\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.ReflectionObjectRenderer's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.proto\.ComposeExtensionsKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KTypeImpl\$arguments\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.compose\.AndroidComposeViewWrapperKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.SourceContext's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KDeclarationContainerImpl\$findPropertyDescriptor\$mostVisibleProperties\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.LayoutInspectorTree\$SubCompositionRoots's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$allStaticMembers\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.ComposeLayoutInspector\$handleGetParametersCommand\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.EmptyContainerForLocal's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.KClassifiers's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.CompositionCallStack's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPropertyImpl\$Companion's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ParameterFactory\$loadConstantsFrom\$[0-9]+\$topClass\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.InternalUnderlyingValOfInlineClass\$Unbound's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KTypeParameterImpl\$upperBounds\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.KAnnotatedElements's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.ParseError's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.ComposeLayoutInspector\$CacheTree's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$allMembers\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KTypeImpl\$arguments\$[0-9]+\$WhenMappings's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.Group's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.KClasses\$allSupertypes\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassifierImpl's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.ReflectionObjectRenderer\$renderFunction\$[0-9]+\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.CachesKt\$K_CLASS_CACHE\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.compose\.AndroidComposeViewWrapper's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$Method\$Static's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.FunctionWithAllInvokes's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$Method\$BoundJvmStaticInObject's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$FieldGetter\$Static's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.TypeOfImplKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.ReflectionObjectRenderer\$WhenMappings's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ReflectionScope's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPackageImpl\$Data\$metadata\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.framework\.ViewExtensionsKt\$flatten\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ParameterFactory\$ParameterCreator\$unwrap\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.FunctionWithAllInvokes\$DefaultImpls's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPackageImpl\$Data\$kotlinClass\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$declaredStaticMembers\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$allNonStaticMembers\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KProperty[0-9]+Impl\$delegateValue\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.InternalUnderlyingValOfInlineClass\$Bound's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ReflectionScope\$Companion's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KProperty[0-9]+Impl\$_getter\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.CacheByClass's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.NodeParameterReference's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.CachesKt\$CACHE_FOR_NULLABLE_BASE_CLASSIFIERS\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.proto\.StringTable's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPackageImpl\$getLocalProperty\$[0-9]+\$[0-9]+\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.KClassifiers\$WhenMappings's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.LayoutInspectorTree\$stitchTreesByLayoutInfo\$[0-9]+\$parentLayout\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.ComposeLayoutInspectorFactory's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.Caller's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.proto\.StringTable\$put\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.InlineClassConverter's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.LayoutInspectorTree\$convert\$group\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.LambdaLocation\$Companion's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$FieldSetter's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KFunctionImpl\$defaultCaller\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.KTypes's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.ReflectJvmMapping's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ParameterFactory\$loadConstantsFrom\$related\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$getLocalProperty\$[0-9]+\$[0-9]+\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPackageImpl\$data\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.InspectorNodeKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.Java[0-9]+RepeatableContainerLoader's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.KCallables\$callSuspend\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.SourceInformationContext's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$Method\$BoundStatic's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.KCallables's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KTypeParameterImpl\$WhenMappings's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ParameterFactory\$ParameterCreator's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$simpleName\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.InlineClassConverter\$notInlineType\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.InternalUnderlyingValOfInlineClass's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.ComposeLayoutInspector\$handleUpdateSettingsCommand\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.ThrowingCaller's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.JvmPropertySignature\$MappedKotlinProperty's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPackageImpl's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$Constructor's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KDeclarationContainerImpl\$getMembers\$visitor\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.util\.IntArrayKt's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.LayoutInspectorTreeKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.KCallablesJvm's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ReflectionScopeKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.JoinedKey's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.CachesKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$descriptor\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.LayoutInspectorTree\$stitchTreesByLayoutInfo\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.JvmFunctionSignature's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.SourceLocation's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPackageImpl\$Data\$scope\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.ReflectJvmMapping\$WhenMappings's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$objectInstance\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.CachesKt\$CACHE_FOR_GENERIC_CLASSIFIERS\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.ComposeLayoutInspector\$getAndroidComposeViews\$roots\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.ComposeLayoutInspector\$CacheData's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.IllegalCallableAccessException's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$declaredMembers\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.JvmFunctionSignature\$JavaConstructor's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.Caller\$DefaultImpls's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.NoSuchPropertyException's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KDeclarationContainerImpl\$Data's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KFunctionImpl's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.MutableInspectorNode's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$FieldSetter\$Instance's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ParameterFactoryKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$FieldGetter\$Instance's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$sealedSubclasses\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.JvmFunctionSignature\$KotlinConstructor's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.CreateKCallableVisitor's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.AnnotationConstructorCaller\$Origin's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$Companion's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.LayoutInspectorTree\$findDeepParentTree\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.UtilKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPropertyImpl\$Getter\$caller\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPropertyImpl\$Setter\$caller\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPropertyImpl\$Setter's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.JvmFunctionSignature\$FakeJavaAnnotationConstructor's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.util\.AnchorMapKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.EmptyGroup's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.BoundCaller's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$WhenMappings's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPackageImpl\$Data's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.ComposeLayoutInspectorKt's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.QuadBounds's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.JvmFunctionSignature\$JavaConstructor\$asString\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.full\.KProperties's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KTypeImpl\$classifier\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.LayoutInspectorTree\$StitchInfo's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.ClassValueCache's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.JvmFunctionSignature\$KotlinFunction's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ParameterFactory\$create\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.SourceLocationInfo's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.androidx\.compose\.ui\.tooling\.data\.CallGroup's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.calls\.CallerImpl\$AccessorForHiddenBoundConstructor's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KCallableImpl\$_typeParameters\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.util\.ThreadUtils's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$nestedClasses\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$supertypes\$[0-9]+\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.NodeParameter's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPropertyImpl\$_descriptor\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KDeclarationContainerImpl\$findPropertyDescriptor\$allMembers\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.ParameterFactory\$ModifierCollector's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.LayoutInspectorTree\$parseLayoutInfo\$\$inlined\$filterIsInstance\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KPropertyImpl\$_javaField\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.proto\.ComposeExtensionsKt\$WhenMappings's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.RawParameter's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.JvmFunctionSignature\$FakeJavaAnnotationConstructor\$special\$\$inlined\$sortedBy\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KTypeParameterOwnerImpl's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.inspector\.LayoutInspectorTree's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.framework\.ViewExtensionsKt's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.UtilKt\$WhenMappings's kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KDeclarationContainerImpl\$Companion's kotlin\.Metadata: null
+Info: Unexpected error while reading androidx\.compose\.ui\.inspection\.compose\.ComposeExtensionsKt\$flatten\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KClassImpl\$Data\$qualifiedName\$[0-9]+'s kotlin\.Metadata: null
+Info: Unexpected error while reading deps\.ui\.inspection\.kotlin\.reflect\.jvm\.internal\.KCallableImpl's kotlin\.Metadata: null
\ No newline at end of file
diff --git a/development/studio/idea.properties b/development/studio/idea.properties
index f352237..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.
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index d082aaf..fe8a291 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -123,6 +123,7 @@
docs("androidx.core:core:1.12.0-alpha04")
docs("androidx.core:core-ktx:1.12.0-alpha03")
docs("androidx.core:core-splashscreen:1.1.0-alpha01")
+ docs("androidx.core:core-telecom:1.0.0-alpha01")
docs("androidx.core:core-testing:1.12.0-alpha03")
docs("androidx.credentials:credentials:1.2.0-alpha03")
docs("androidx.credentials:credentials-play-services-auth:1.2.0-alpha03")
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index e62e0d7..5a668e2 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -133,6 +133,7 @@
docs(project(":core:core-remoteviews"))
docs(project(":core:core-splashscreen"))
docs(project(":core:core-role"))
+ docs(project(":core:core-telecom"))
docs(project(":core:core-testing"))
docs(project(":core:uwb:uwb"))
docs(project(":core:uwb:uwb-rxjava3"))
@@ -178,6 +179,7 @@
docs(project(":glance:glance-wear-tiles"))
docs(project(":graphics:filters:filters"))
docs(project(":graphics:graphics-core"))
+ docs(project(":graphics:graphics-path"))
docs(project(":graphics:graphics-shapes"))
docs(project(":gridlayout:gridlayout"))
docs(project(":health:connect:connect-client"))
@@ -366,12 +368,12 @@
docs(project(":wear:watchface:watchface-style"))
docs(project(":webkit:webkit"))
docs(project(":window:window"))
+ samples(project(":window:window-samples"))
docs(project(":window:window-core"))
docs(project(":window:window-java"))
docs(project(":window:window-rxjava2"))
docs(project(":window:window-rxjava3"))
stubs(project(":window:sidecar:sidecar"))
- samples(project(":window:window-samples"))
stubs(project(":window:extensions:extensions"))
stubs(project(":window:extensions:core:core"))
docs(project(":window:window-testing"))
diff --git a/documentfile/documentfile/build.gradle b/documentfile/documentfile/build.gradle
index 4993c26..f9ab3ec 100644
--- a/documentfile/documentfile/build.gradle
+++ b/documentfile/documentfile/build.gradle
@@ -17,7 +17,7 @@
}
androidx {
- name = "Android Support Library Document File"
+ name = "Document File"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/draganddrop/draganddrop/build.gradle b/draganddrop/draganddrop/build.gradle
index 37b0365..41c3462 100644
--- a/draganddrop/draganddrop/build.gradle
+++ b/draganddrop/draganddrop/build.gradle
@@ -44,7 +44,7 @@
}
androidx {
- name = "AndroidX Drag and Drop Library"
+ name = "Drag and Drop"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2021"
description = "This library makes it easy for developers to accept data dragged-and-dropped from another app, and show a consistent affordance."
diff --git a/drawerlayout/drawerlayout/api/api_lint.ignore b/drawerlayout/drawerlayout/api/api_lint.ignore
index be4e831..69b398e 100644
--- a/drawerlayout/drawerlayout/api/api_lint.ignore
+++ b/drawerlayout/drawerlayout/api/api_lint.ignore
@@ -3,12 +3,6 @@
Parameter type is concrete collection (`java.util.ArrayList`); must be higher-level interface
-InvalidNullabilityOverride: androidx.drawerlayout.widget.DrawerLayout#drawChild(android.graphics.Canvas, android.view.View, long) parameter #0:
- Invalid nullability on parameter `canvas` in method `drawChild`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.drawerlayout.widget.DrawerLayout#onDraw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `c` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-
-
ListenerInterface: androidx.drawerlayout.widget.DrawerLayout.SimpleDrawerListener:
Listeners should be an interface, or otherwise renamed Callback: SimpleDrawerListener
@@ -23,6 +17,8 @@
Missing nullability on parameter `p` in method `checkLayoutParams`
MissingNullability: androidx.drawerlayout.widget.DrawerLayout#dispatchGenericMotionEvent(android.view.MotionEvent) parameter #0:
Missing nullability on parameter `event` in method `dispatchGenericMotionEvent`
+MissingNullability: androidx.drawerlayout.widget.DrawerLayout#drawChild(android.graphics.Canvas, android.view.View, long) parameter #0:
+ Missing nullability on parameter `canvas` in method `drawChild`
MissingNullability: androidx.drawerlayout.widget.DrawerLayout#drawChild(android.graphics.Canvas, android.view.View, long) parameter #1:
Missing nullability on parameter `child` in method `drawChild`
MissingNullability: androidx.drawerlayout.widget.DrawerLayout#generateDefaultLayoutParams():
@@ -35,6 +31,8 @@
Missing nullability on method `generateLayoutParams` return
MissingNullability: androidx.drawerlayout.widget.DrawerLayout#generateLayoutParams(android.view.ViewGroup.LayoutParams) parameter #0:
Missing nullability on parameter `p` in method `generateLayoutParams`
+MissingNullability: androidx.drawerlayout.widget.DrawerLayout#onDraw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `c` in method `onDraw`
MissingNullability: androidx.drawerlayout.widget.DrawerLayout#onInterceptTouchEvent(android.view.MotionEvent) parameter #0:
Missing nullability on parameter `ev` in method `onInterceptTouchEvent`
MissingNullability: androidx.drawerlayout.widget.DrawerLayout#onKeyDown(int, android.view.KeyEvent) parameter #1:
diff --git a/drawerlayout/drawerlayout/build.gradle b/drawerlayout/drawerlayout/build.gradle
index 6405b31..2e717e7 100644
--- a/drawerlayout/drawerlayout/build.gradle
+++ b/drawerlayout/drawerlayout/build.gradle
@@ -22,7 +22,7 @@
}
androidx {
- name = "Android Support Library Drawer Layout"
+ name = "Drawer Layout"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/dynamicanimation/dynamicanimation/build.gradle b/dynamicanimation/dynamicanimation/build.gradle
index b3a6d81..29f51a5 100644
--- a/dynamicanimation/dynamicanimation/build.gradle
+++ b/dynamicanimation/dynamicanimation/build.gradle
@@ -19,7 +19,7 @@
}
androidx {
- name = "Android Support DynamicAnimation"
+ name = "DynamicAnimation"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.DYNAMICANIMATION
inceptionYear = "2017"
diff --git a/emoji/emoji-appcompat/build.gradle b/emoji/emoji-appcompat/build.gradle
index df0af37..b0e39af 100644
--- a/emoji/emoji-appcompat/build.gradle
+++ b/emoji/emoji-appcompat/build.gradle
@@ -27,7 +27,7 @@
}
androidx {
- name = "Android Emoji AppCompat"
+ name = "Emoji AppCompat"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.EMOJI
inceptionYear = "2017"
diff --git a/emoji/emoji-bundled/build.gradle b/emoji/emoji-bundled/build.gradle
index d757fb1..3414c36 100644
--- a/emoji/emoji-bundled/build.gradle
+++ b/emoji/emoji-bundled/build.gradle
@@ -21,7 +21,7 @@
}
androidx {
- name = "Android Emoji Compat"
+ name = "Emoji Bundled"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.EMOJI
inceptionYear = "2017"
diff --git a/emoji/emoji/build.gradle b/emoji/emoji/build.gradle
index 5e135b6..cfb14e4 100644
--- a/emoji/emoji/build.gradle
+++ b/emoji/emoji/build.gradle
@@ -54,7 +54,7 @@
}
androidx {
- name = "Android Emoji Compat"
+ name = "Emoji"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.EMOJI
inceptionYear = "2017"
diff --git a/emoji2/emoji2-bundled/build.gradle b/emoji2/emoji2-bundled/build.gradle
index 5be609e..da5a158 100644
--- a/emoji2/emoji2-bundled/build.gradle
+++ b/emoji2/emoji2-bundled/build.gradle
@@ -46,7 +46,7 @@
}
androidx {
- name = "Android Emoji2 Compat"
+ name = "Emoji2 Bundled"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Library bundled with assets to enable emoji compatibility in Kitkat and newer " +
diff --git a/emoji2/emoji2-emojipicker/build.gradle b/emoji2/emoji2-emojipicker/build.gradle
index 2ab0c6a..0761e00 100644
--- a/emoji2/emoji2-emojipicker/build.gradle
+++ b/emoji2/emoji2-emojipicker/build.gradle
@@ -57,7 +57,7 @@
}
androidx {
- name = "androidx.emoji2:emoji2-emojipicker"
+ name = "Emoji2 Emoji Picker"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2022"
description = "This library provides the latest emoji support and emoji picker UI to input " +
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt
index b1e7b2f..e000eb32 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt
@@ -159,9 +159,9 @@
private val radius = resources.getDimension(R.dimen.emoji_picker_skin_tone_circle_radius)
var paint: Paint? = null
- override fun draw(canvas: Canvas?) {
+ override fun draw(canvas: Canvas) {
super.draw(canvas)
- canvas?.apply {
+ canvas.apply {
paint?.let { drawCircle(width / 2f, height / 2f, radius, it) }
}
}
diff --git a/emoji2/emoji2-views-helper/build.gradle b/emoji2/emoji2-views-helper/build.gradle
index ed51ab7..62b7b69 100644
--- a/emoji2/emoji2-views-helper/build.gradle
+++ b/emoji2/emoji2-views-helper/build.gradle
@@ -27,8 +27,8 @@
}
androidx {
- name = "Android Emoji2 Compat view helpers"
+ name = "Emoji2 Views Helper"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
- description = "View helpers for Emoji2"
+ description = "Provide helper classes for Emoji2 views."
}
diff --git a/emoji2/emoji2-views/build.gradle b/emoji2/emoji2-views/build.gradle
index 597d3b1..eae7eb2 100644
--- a/emoji2/emoji2-views/build.gradle
+++ b/emoji2/emoji2-views/build.gradle
@@ -33,7 +33,7 @@
}
androidx {
- name = "Android Emoji2 Compat Views"
+ name = "Emoji2 Views"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Support for using emoji2 directly with Android Views, for use in apps without " +
diff --git a/emoji2/emoji2/build.gradle b/emoji2/emoji2/build.gradle
index f1a2cdd..3ab8724 100644
--- a/emoji2/emoji2/build.gradle
+++ b/emoji2/emoji2/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "Android Emoji2 Compat"
+ name = "Emoji2"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Core library to enable emoji compatibility in Kitkat and newer devices to avoid the empty emoji characters."
diff --git a/exifinterface/exifinterface/build.gradle b/exifinterface/exifinterface/build.gradle
index b6bcaab..12e623a 100644
--- a/exifinterface/exifinterface/build.gradle
+++ b/exifinterface/exifinterface/build.gradle
@@ -16,7 +16,7 @@
}
androidx {
- name = "Android Support ExifInterface"
+ name = "ExifInterface"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2016"
description = "Android Support ExifInterface"
diff --git a/fragment/fragment-lint/build.gradle b/fragment/fragment-lint/build.gradle
index 9a091e2..b4f8162 100644
--- a/fragment/fragment-lint/build.gradle
+++ b/fragment/fragment-lint/build.gradle
@@ -36,7 +36,7 @@
}
androidx {
- name = "Android Fragment Lint Checks"
+ name = "Fragment Lint Checks"
type = LibraryType.LINT
inceptionYear = "2019"
description = "Android Fragment Lint Checks"
diff --git a/fragment/fragment-testing-lint/build.gradle b/fragment/fragment-testing-lint/build.gradle
index 91a6a84..a9641c7 100644
--- a/fragment/fragment-testing-lint/build.gradle
+++ b/fragment/fragment-testing-lint/build.gradle
@@ -33,7 +33,7 @@
}
androidx {
- name = "Android Fragment-Testing Lint Checks"
+ name = "Fragment-Testing Lint Checks"
type = LibraryType.LINT
inceptionYear = "2019"
description = "Lint Checks for the Fragment Testing module"
diff --git a/fragment/fragment-testing-manifest-lint/build.gradle b/fragment/fragment-testing-manifest-lint/build.gradle
index 3f8af05..9b4b9f4 100644
--- a/fragment/fragment-testing-manifest-lint/build.gradle
+++ b/fragment/fragment-testing-manifest-lint/build.gradle
@@ -33,7 +33,7 @@
}
androidx {
- name = "Android Fragment-Testing-Manifest Lint Checks"
+ name = "Fragment-Testing-Manifest Lint Checks"
type = LibraryType.LINT
inceptionYear = "2022"
description = "Lint Checks for the Fragment Testing Manifest module"
diff --git a/fragment/fragment-testing/build.gradle b/fragment/fragment-testing/build.gradle
index 4ef2ed4..0230108 100644
--- a/fragment/fragment-testing/build.gradle
+++ b/fragment/fragment-testing/build.gradle
@@ -43,7 +43,7 @@
}
androidx {
- name = "Fragment Test Extensions"
+ name = "Fragment Testing Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Extensions for testing 'fragment' artifact"
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 91b6c0f..e2863b8 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -71,7 +71,7 @@
}
androidx {
- name = "Android Support Library fragment"
+ name = "fragment"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2011"
description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\'t a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/glance/glance-appwidget-preview/build.gradle b/glance/glance-appwidget-preview/build.gradle
index 07567f5..20fd21c 100644
--- a/glance/glance-appwidget-preview/build.gradle
+++ b/glance/glance-appwidget-preview/build.gradle
@@ -52,7 +52,7 @@
}
androidx {
- name = "Android Glance AppWidget Preview"
+ name = "Glance AppWidget Preview"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.GLANCE_PREVIEW
inceptionYear = "2022"
diff --git a/glance/glance-appwidget/src/test/resources/robolectric.properties b/glance/glance-appwidget/src/test/resources/robolectric.properties
index ab64ba7..17db863 100644
--- a/glance/glance-appwidget/src/test/resources/robolectric.properties
+++ b/glance/glance-appwidget/src/test/resources/robolectric.properties
@@ -1,3 +1,3 @@
-# Robolectric currently doesn't support API 31, so we have to explicitly specify 30 as the target
-# sdk for now. Remove when no longer necessary.
+# robolectric properties
+# Temporary until Glance team fixes their tests to work against sdk=33 (b/281041185).
sdk=30
diff --git a/glance/glance-material/build.gradle b/glance/glance-material/build.gradle
index 5a02590..17a787b 100644
--- a/glance/glance-material/build.gradle
+++ b/glance/glance-material/build.gradle
@@ -26,7 +26,7 @@
}
androidx {
- name = "Android Glance Material"
+ name = "Glance Material"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2022"
description = "Glance Material 2 integration library." +
diff --git a/glance/glance-material3/build.gradle b/glance/glance-material3/build.gradle
index 4177903..9439749 100644
--- a/glance/glance-material3/build.gradle
+++ b/glance/glance-material3/build.gradle
@@ -26,7 +26,7 @@
}
androidx {
- name = "Android Glance Material"
+ name = "Glance Material"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2022"
description = "Glance Material integration library." +
diff --git a/glance/glance-preview/build.gradle b/glance/glance-preview/build.gradle
index 844dcee..0857b2ee 100644
--- a/glance/glance-preview/build.gradle
+++ b/glance/glance-preview/build.gradle
@@ -24,7 +24,7 @@
}
androidx {
- name = "Android Glance Preview"
+ name = "Glance Preview"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.GLANCE_PREVIEW
inceptionYear = "2022"
diff --git a/glance/glance-template/build.gradle b/glance/glance-template/build.gradle
index a74f078..331d3e2 100644
--- a/glance/glance-template/build.gradle
+++ b/glance/glance-template/build.gradle
@@ -73,7 +73,7 @@
}
androidx {
- name = "Glance Templates Library"
+ name = "Glance Templates"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.GLANCE_TEMPLATE
inceptionYear = "2021"
diff --git a/glance/glance-wear-tiles-preview/build.gradle b/glance/glance-wear-tiles-preview/build.gradle
index a5836b4..9314528 100644
--- a/glance/glance-wear-tiles-preview/build.gradle
+++ b/glance/glance-wear-tiles-preview/build.gradle
@@ -51,7 +51,7 @@
}
androidx {
- name = "Android Glance Wear Tiles Preview"
+ name = "Glance Wear Tiles Preview"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.GLANCE_WEAR_TILES
inceptionYear = "2022"
diff --git a/glance/glance-wear-tiles/src/test/resources/robolectric.properties b/glance/glance-wear-tiles/src/test/resources/robolectric.properties
index 80e2a6f..69fde47 100644
--- a/glance/glance-wear-tiles/src/test/resources/robolectric.properties
+++ b/glance/glance-wear-tiles/src/test/resources/robolectric.properties
@@ -1 +1,3 @@
# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/glance/glance/build.gradle b/glance/glance/build.gradle
index 68ec157..1d44a68 100644
--- a/glance/glance/build.gradle
+++ b/glance/glance/build.gradle
@@ -88,7 +88,7 @@
}
androidx {
- name = "Glance Core Library"
+ name = "Glance"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2021"
description = "Glance allows developers to build layouts for remote surfaces using a Jetpack " +
diff --git a/glance/glance/src/test/resources/robolectric.properties b/glance/glance/src/test/resources/robolectric.properties
index 80e2a6f..69fde47 100644
--- a/glance/glance/src/test/resources/robolectric.properties
+++ b/glance/glance/src/test/resources/robolectric.properties
@@ -1 +1,3 @@
# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/gradle.properties b/gradle.properties
index 47646f4..1574323 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -24,17 +24,17 @@
android.forceJacocoOutOfProcess=true
android.experimental.lint.missingBaselineIsEmptyBaseline=true
-# Generate versioned API files
-androidx.writeVersionedApiFiles=true
+# Don't generate versioned API files
+androidx.writeVersionedApiFiles=false
-# Run the CheckAarMetadata task
-android.experimental.disableCompileSdkChecks=false
+# Don't run the CheckAarMetadata task
+android.experimental.disableCompileSdkChecks=true
-# Do restrict compileSdkPreview usage
-androidx.allowCustomCompileSdk=false
+# Don't restrict compileSdkPreview usage
+androidx.allowCustomCompileSdk=true
# Don't warn about needing to update AGP
-android.suppressUnsupportedCompileSdk=UpsideDownCake,VanillaIceCream,33
+android.suppressUnsupportedCompileSdk=UpsideDownCake,VanillaIceCream,33,34
# Disable features we do not use
android.defaults.buildfeatures.aidl=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 2aacccb..d34e240 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -221,6 +221,7 @@
nullaway = { module = "com.uber.nullaway:nullaway", version = "0.3.7" }
okhttpMockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version = "3.14.7" }
okio = { module = "com.squareup.okio:okio", version = "3.1.0" }
+opentest4j = { module = "org.opentest4j:opentest4j", version = "1.2.0" }
playFeatureDelivery = { module = "com.google.android.play:feature-delivery", version = "2.0.1" }
playCore = { module = "com.google.android.play:core", version = "1.10.3" }
playServicesAuth = {module = "com.google.android.gms:play-services-auth", version = "20.5.0"}
diff --git a/graphics/graphics-core/api/current.txt b/graphics/graphics-core/api/current.txt
index 0079079..bad86cb 100644
--- a/graphics/graphics-core/api/current.txt
+++ b/graphics/graphics-core/api/current.txt
@@ -297,6 +297,8 @@
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBufferTransform(androidx.graphics.surface.SurfaceControlCompat surfaceControl, int transformation);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setCrop(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.graphics.Rect? crop);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setDamageRegion(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.graphics.Region? region);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public androidx.graphics.surface.SurfaceControlCompat.Transaction setDataSpace(androidx.graphics.surface.SurfaceControlCompat surfaceControl, int dataSpace);
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public androidx.graphics.surface.SurfaceControlCompat.Transaction setExtendedRangeBrightness(androidx.graphics.surface.SurfaceControlCompat surfaceControl, @FloatRange(from=1.0, fromInclusive=true) float currentBufferRatio, @FloatRange(from=1.0, fromInclusive=true) float desiredRatio);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setLayer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, int z);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setOpaque(androidx.graphics.surface.SurfaceControlCompat surfaceControl, boolean isOpaque);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setPosition(androidx.graphics.surface.SurfaceControlCompat surfaceControl, float x, float y);
diff --git a/graphics/graphics-core/api/public_plus_experimental_current.txt b/graphics/graphics-core/api/public_plus_experimental_current.txt
index 0079079..bad86cb 100644
--- a/graphics/graphics-core/api/public_plus_experimental_current.txt
+++ b/graphics/graphics-core/api/public_plus_experimental_current.txt
@@ -297,6 +297,8 @@
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBufferTransform(androidx.graphics.surface.SurfaceControlCompat surfaceControl, int transformation);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setCrop(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.graphics.Rect? crop);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setDamageRegion(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.graphics.Region? region);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public androidx.graphics.surface.SurfaceControlCompat.Transaction setDataSpace(androidx.graphics.surface.SurfaceControlCompat surfaceControl, int dataSpace);
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public androidx.graphics.surface.SurfaceControlCompat.Transaction setExtendedRangeBrightness(androidx.graphics.surface.SurfaceControlCompat surfaceControl, @FloatRange(from=1.0, fromInclusive=true) float currentBufferRatio, @FloatRange(from=1.0, fromInclusive=true) float desiredRatio);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setLayer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, int z);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setOpaque(androidx.graphics.surface.SurfaceControlCompat surfaceControl, boolean isOpaque);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setPosition(androidx.graphics.surface.SurfaceControlCompat surfaceControl, float x, float y);
diff --git a/graphics/graphics-core/api/restricted_current.txt b/graphics/graphics-core/api/restricted_current.txt
index f265e02..944121b 100644
--- a/graphics/graphics-core/api/restricted_current.txt
+++ b/graphics/graphics-core/api/restricted_current.txt
@@ -298,6 +298,8 @@
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBufferTransform(androidx.graphics.surface.SurfaceControlCompat surfaceControl, int transformation);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setCrop(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.graphics.Rect? crop);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setDamageRegion(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.graphics.Region? region);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public androidx.graphics.surface.SurfaceControlCompat.Transaction setDataSpace(androidx.graphics.surface.SurfaceControlCompat surfaceControl, int dataSpace);
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public androidx.graphics.surface.SurfaceControlCompat.Transaction setExtendedRangeBrightness(androidx.graphics.surface.SurfaceControlCompat surfaceControl, @FloatRange(from=1.0, fromInclusive=true) float currentBufferRatio, @FloatRange(from=1.0, fromInclusive=true) float desiredRatio);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setLayer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, int z);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setOpaque(androidx.graphics.surface.SurfaceControlCompat surfaceControl, boolean isOpaque);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setPosition(androidx.graphics.surface.SurfaceControlCompat surfaceControl, float x, float y);
diff --git a/graphics/graphics-core/build.gradle b/graphics/graphics-core/build.gradle
index f20971b..22bc7e9 100644
--- a/graphics/graphics-core/build.gradle
+++ b/graphics/graphics-core/build.gradle
@@ -26,6 +26,7 @@
dependencies {
api(libs.kotlinStdlib)
implementation 'androidx.annotation:annotation:1.2.0'
+ implementation("androidx.core:core:1.8.0")
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
@@ -61,7 +62,7 @@
}
androidx {
- name = "Android Graphics Core"
+ name = "Graphics Core"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.GRAPHICS_CORE
inceptionYear = "2021"
diff --git a/graphics/graphics-core/lint-baseline.xml b/graphics/graphics-core/lint-baseline.xml
index e92cd67..85651b3 100644
--- a/graphics/graphics-core/lint-baseline.xml
+++ b/graphics/graphics-core/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.1.0-alpha07" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-beta03)" variant="all" version="8.1.0-alpha07">
+<issues format="6" by="lint 8.1.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta01)" variant="all" version="8.1.0-beta01">
<issue
id="BanHideAnnotation"
@@ -28,4 +28,13 @@
file="src/main/java/androidx/opengl/EGLExt.kt"/>
</issue>
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" return if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRenderer.kt"/>
+ </issue>
+
</issues>
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/BufferTransformerTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/BufferTransformerTest.kt
index 786a9fa7..ff9a987 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/BufferTransformerTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/BufferTransformerTest.kt
@@ -52,6 +52,7 @@
val expected = createMatrix()
assertEquals(transform.transform.size, SIZE)
assertIsEqual(transform.transform, expected)
+ assertEquals(BUFFER_TRANSFORM_IDENTITY, transform.computedTransform)
}
@Test
@@ -69,6 +70,7 @@
}
)
assertIsEqual(transform.transform, expected)
+ assertEquals(BUFFER_TRANSFORM_ROTATE_90, transform.computedTransform)
}
@Test
@@ -86,6 +88,7 @@
}
)
assertIsEqual(transform.transform, expected)
+ assertEquals(BUFFER_TRANSFORM_ROTATE_180, transform.computedTransform)
}
@Test
@@ -103,6 +106,7 @@
}
)
assertIsEqual(transform.transform, expected)
+ assertEquals(BUFFER_TRANSFORM_ROTATE_270, transform.computedTransform)
}
@Test
@@ -115,6 +119,7 @@
val expected = createMatrix()
assertEquals(transform.transform.size, SIZE)
assertIsEqual(transform.transform, expected)
+ assertEquals(BufferTransformHintResolver.UNKNOWN_TRANSFORM, transform.computedTransform)
}
private inline fun createMatrix(block: FloatArray.() -> Unit = {}): FloatArray =
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV34Test.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV34Test.kt
new file mode 100644
index 0000000..b1ade98
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV34Test.kt
@@ -0,0 +1,413 @@
+/*
+ * 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.graphics.lowlatency
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorSpace
+import android.hardware.HardwareBuffer
+import android.os.Build
+import androidx.core.os.BuildCompat
+import androidx.graphics.drawSquares
+import androidx.graphics.isAllColor
+import androidx.graphics.opengl.egl.supportsNativeAndroidFence
+import androidx.graphics.surface.SurfaceControlCompat
+import androidx.graphics.surface.SurfaceControlCompat.Companion.BUFFER_TRANSFORM_IDENTITY
+import androidx.graphics.verifyQuadrants
+import androidx.graphics.withEgl
+import androidx.hardware.SyncFenceCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class SingleBufferedCanvasRendererV34Test {
+
+ companion object {
+ const val TEST_WIDTH = 20
+ const val TEST_HEIGHT = 20
+ }
+
+ data class RectColors(
+ val topLeft: Int,
+ val topRight: Int,
+ val bottomLeft: Int,
+ val bottomRight: Int
+ )
+
+ @Test
+ fun testRenderFrameRotate0() {
+ testRenderWithTransform(
+ BUFFER_TRANSFORM_IDENTITY,
+ RectColors(
+ topLeft = Color.RED,
+ topRight = Color.YELLOW,
+ bottomRight = Color.BLUE,
+ bottomLeft = Color.GREEN
+ ),
+ RectColors(
+ topLeft = Color.RED,
+ topRight = Color.YELLOW,
+ bottomRight = Color.BLUE,
+ bottomLeft = Color.GREEN
+ )
+ )
+ }
+
+ @Test
+ fun testRenderFrameRotate90() {
+ testRenderWithTransform(
+ SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_90,
+ RectColors(
+ topLeft = Color.RED,
+ topRight = Color.YELLOW,
+ bottomRight = Color.BLUE,
+ bottomLeft = Color.GREEN
+ ),
+ RectColors(
+ topLeft = Color.YELLOW,
+ topRight = Color.BLUE,
+ bottomRight = Color.GREEN,
+ bottomLeft = Color.RED
+ )
+ )
+ }
+
+ @Test
+ fun testRenderFrameRotate180() {
+ testRenderWithTransform(
+ SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_180,
+ RectColors(
+ topLeft = Color.RED,
+ topRight = Color.YELLOW,
+ bottomRight = Color.BLUE,
+ bottomLeft = Color.GREEN
+ ),
+ RectColors(
+ topLeft = Color.BLUE,
+ topRight = Color.GREEN,
+ bottomRight = Color.RED,
+ bottomLeft = Color.YELLOW
+ )
+ )
+ }
+
+ @Test
+ fun testRenderFrameRotate270() {
+ testRenderWithTransform(
+ SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_270,
+ RectColors(
+ topLeft = Color.RED,
+ topRight = Color.YELLOW,
+ bottomRight = Color.BLUE,
+ bottomLeft = Color.GREEN
+ ),
+ RectColors(
+ topLeft = Color.GREEN,
+ topRight = Color.RED,
+ bottomRight = Color.YELLOW,
+ bottomLeft = Color.BLUE
+ )
+ )
+ }
+
+ @Test
+ fun testClearRenderer() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val transformer = BufferTransformer().apply {
+ computeTransform(TEST_WIDTH, TEST_HEIGHT, BUFFER_TRANSFORM_IDENTITY)
+ }
+ val executor = Executors.newSingleThreadExecutor()
+ val firstRenderLatch = CountDownLatch(1)
+ val clearLatch = CountDownLatch(2)
+ var buffer: HardwareBuffer? = null
+ val renderer = SingleBufferedCanvasRendererV34(
+ TEST_WIDTH,
+ TEST_HEIGHT,
+ transformer,
+ executor,
+ object : SingleBufferedCanvasRenderer.RenderCallbacks<Unit> {
+ override fun render(canvas: Canvas, width: Int, height: Int, param: Unit) {
+ canvas.drawColor(Color.RED)
+ }
+
+ override fun onBufferReady(
+ hardwareBuffer: HardwareBuffer,
+ syncFenceCompat: SyncFenceCompat?
+ ) {
+ syncFenceCompat?.awaitForever()
+ buffer = hardwareBuffer
+ firstRenderLatch.countDown()
+ clearLatch.countDown()
+ }
+ })
+ try {
+ renderer.render(Unit)
+ firstRenderLatch.await(3000, TimeUnit.MILLISECONDS)
+ renderer.clear()
+ assertTrue(clearLatch.await(3000, TimeUnit.MILLISECONDS))
+ assertNotNull(buffer)
+ val colorSpace = ColorSpace.get(ColorSpace.Named.LINEAR_SRGB)
+ val bitmap = Bitmap.wrapHardwareBuffer(buffer!!, colorSpace)
+ ?.copy(Bitmap.Config.ARGB_8888, false)
+ assertNotNull(bitmap)
+ assertTrue(bitmap!!.isAllColor(Color.TRANSPARENT))
+ } finally {
+ val latch = CountDownLatch(1)
+ renderer.release(true) {
+ executor.shutdownNow()
+ latch.countDown()
+ }
+ assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+ }
+ }
+
+ @Test
+ fun testCancelPending() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val transformer = BufferTransformer().apply {
+ computeTransform(TEST_WIDTH, TEST_HEIGHT, BUFFER_TRANSFORM_IDENTITY)
+ }
+ val executor = Executors.newSingleThreadExecutor()
+ var buffer: HardwareBuffer? = null
+ val initialDrawLatch = CountDownLatch(1)
+
+ var drawCancelledRequestLatch: CountDownLatch? = null
+ val renderer = SingleBufferedCanvasRendererV34(
+ TEST_WIDTH,
+ TEST_HEIGHT,
+ transformer,
+ executor,
+ object : SingleBufferedCanvasRenderer.RenderCallbacks<Int> {
+ override fun render(canvas: Canvas, width: Int, height: Int, param: Int) {
+ canvas.drawColor(param)
+ }
+
+ override fun onBufferReady(
+ hardwareBuffer: HardwareBuffer,
+ syncFenceCompat: SyncFenceCompat?
+ ) {
+ syncFenceCompat?.awaitForever()
+ buffer = hardwareBuffer
+ initialDrawLatch.countDown()
+ drawCancelledRequestLatch?.countDown()
+ }
+ })
+ try {
+ renderer.render(Color.RED)
+ assertTrue(initialDrawLatch.await(3000, TimeUnit.MILLISECONDS))
+
+ drawCancelledRequestLatch = CountDownLatch(2)
+ renderer.render(Color.BLUE)
+ renderer.render(Color.BLACK)
+ renderer.cancelPending()
+
+ // Because the requests were cancelled this latch should not be signalled
+ assertFalse(drawCancelledRequestLatch.await(1000, TimeUnit.MILLISECONDS))
+ assertNotNull(buffer)
+ val colorSpace = ColorSpace.get(ColorSpace.Named.LINEAR_SRGB)
+ val bitmap = Bitmap.wrapHardwareBuffer(buffer!!, colorSpace)
+ ?.copy(Bitmap.Config.ARGB_8888, false)
+ assertNotNull(bitmap)
+ assertTrue(bitmap!!.isAllColor(Color.RED))
+ } finally {
+ val latch = CountDownLatch(1)
+ renderer.release(true) {
+ executor.shutdownNow()
+ latch.countDown()
+ }
+ assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+ }
+ }
+
+ @Test
+ fun testMultiReleasesDoesNotCrash() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val transformer = BufferTransformer().apply {
+ computeTransform(TEST_WIDTH, TEST_HEIGHT, BUFFER_TRANSFORM_IDENTITY)
+ }
+ val executor = Executors.newSingleThreadExecutor()
+ val renderer = SingleBufferedCanvasRendererV34(
+ TEST_WIDTH,
+ TEST_HEIGHT,
+ transformer,
+ executor,
+ object : SingleBufferedCanvasRenderer.RenderCallbacks<Void> {
+ override fun render(canvas: Canvas, width: Int, height: Int, param: Void) {
+ // NO-OP
+ }
+
+ override fun onBufferReady(
+ hardwareBuffer: HardwareBuffer,
+ syncFenceCompat: SyncFenceCompat?
+ ) {
+ // NO-OP
+ }
+ })
+ try {
+ val latch = CountDownLatch(1)
+ renderer.release(true) {
+ executor.shutdownNow()
+ latch.countDown()
+ }
+ assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+ renderer.release(true)
+ } finally {
+ if (!executor.isShutdown) {
+ executor.shutdownNow()
+ }
+ }
+ }
+
+ @Test
+ fun testRendererVisibleFlag() {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ var supportsNativeAndroidFence = false
+ withEgl { eglManager ->
+ supportsNativeAndroidFence = eglManager.supportsNativeAndroidFence()
+ }
+ if (!supportsNativeAndroidFence) {
+ return
+ }
+ val transformer = BufferTransformer().apply {
+ computeTransform(TEST_WIDTH, TEST_HEIGHT, BUFFER_TRANSFORM_IDENTITY)
+ }
+ val executor = Executors.newSingleThreadExecutor()
+ var syncFenceNull = false
+ var drawLatch: CountDownLatch? = null
+ val renderer = SingleBufferedCanvasRendererV34(
+ TEST_WIDTH,
+ TEST_HEIGHT,
+ transformer,
+ executor,
+ object : SingleBufferedCanvasRenderer.RenderCallbacks<Int> {
+ override fun render(canvas: Canvas, width: Int, height: Int, param: Int) {
+ canvas.drawColor(param)
+ }
+
+ override fun onBufferReady(
+ hardwareBuffer: HardwareBuffer,
+ syncFenceCompat: SyncFenceCompat?
+ ) {
+ syncFenceNull = syncFenceCompat == null
+ syncFenceCompat?.awaitForever()
+ drawLatch?.countDown()
+ }
+ })
+ try {
+ renderer.isVisible = false
+ drawLatch = CountDownLatch(1)
+ renderer.render(Color.RED)
+ assertTrue(drawLatch.await(3000, TimeUnit.MILLISECONDS))
+ assertFalse(syncFenceNull)
+
+ renderer.isVisible = true
+ drawLatch = CountDownLatch(1)
+ renderer.render(Color.BLUE)
+ assertTrue(drawLatch.await(3000, TimeUnit.MILLISECONDS))
+ } finally {
+ val latch = CountDownLatch(1)
+ renderer.release(true) {
+ executor.shutdownNow()
+ latch.countDown()
+ }
+ assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+ }
+ }
+
+ private fun testRenderWithTransform(
+ transform: Int,
+ actualColors: RectColors,
+ expectedColors: RectColors
+ ) {
+ if (!BuildCompat.isAtLeastU()) {
+ return
+ }
+ val transformer = BufferTransformer()
+ transformer.computeTransform(TEST_WIDTH, TEST_HEIGHT, transform)
+ val executor = Executors.newSingleThreadExecutor()
+ var buffer: HardwareBuffer? = null
+ val renderLatch = CountDownLatch(1)
+ val renderer = SingleBufferedCanvasRendererV34(
+ TEST_WIDTH,
+ TEST_HEIGHT,
+ transformer,
+ executor,
+ object : SingleBufferedCanvasRenderer.RenderCallbacks<Int> {
+ override fun render(canvas: Canvas, width: Int, height: Int, param: Int) {
+ drawSquares(
+ canvas,
+ width,
+ height,
+ actualColors.topLeft,
+ actualColors.topRight,
+ actualColors.bottomLeft,
+ actualColors.bottomRight
+ )
+ }
+
+ override fun onBufferReady(
+ hardwareBuffer: HardwareBuffer,
+ syncFenceCompat: SyncFenceCompat?
+ ) {
+ syncFenceCompat?.awaitForever()
+ buffer = hardwareBuffer
+ renderLatch.countDown()
+ }
+ })
+ try {
+ renderer.render(0)
+ assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
+ assertNotNull(buffer)
+ val colorSpace = ColorSpace.get(ColorSpace.Named.LINEAR_SRGB)
+ val bitmap = Bitmap.wrapHardwareBuffer(buffer!!, colorSpace)
+ ?.copy(Bitmap.Config.ARGB_8888, false)
+ assertNotNull(bitmap)
+ bitmap!!.verifyQuadrants(
+ expectedColors.topLeft,
+ expectedColors.topRight,
+ expectedColors.bottomLeft,
+ expectedColors.bottomRight
+ )
+ } finally {
+ val latch = CountDownLatch(1)
+ renderer.release(true) {
+ executor.shutdownNow()
+ latch.countDown()
+ }
+ assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+ }
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
index 4d9d4b8..e4715d6 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
@@ -16,19 +16,23 @@
package androidx.graphics.surface
+import android.annotation.SuppressLint
import android.graphics.Color
import android.graphics.ColorSpace
import android.graphics.Rect
import android.graphics.Region
+import android.hardware.DataSpace
import android.opengl.EGL14
import android.os.Build
import android.os.SystemClock
+import android.view.Display
import android.view.SurfaceHolder
import androidx.graphics.opengl.egl.EGLConfigAttributes
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
import androidx.graphics.opengl.egl.EGLVersion
import androidx.graphics.opengl.egl.supportsNativeAndroidFence
+import androidx.graphics.surface.SurfaceControlUtils.Companion.getSolidBuffer
import androidx.hardware.SyncFenceCompat
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
@@ -40,9 +44,11 @@
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
+import java.util.function.Consumer
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Before
@@ -1891,6 +1897,165 @@
}
}
+ @SuppressLint("NewApi")
+ @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ fun testSetExtendedRangeBrightnessThrowsOnUnsupportedPlatforms() {
+ ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
+ .moveToState(
+ Lifecycle.State.CREATED
+ ).onActivity {
+ val callback = object : SurfaceHolderCallback() {
+ override fun surfaceCreated(sh: SurfaceHolder) {
+
+ assertThrows(UnsupportedOperationException::class.java) {
+ val surfaceControl = SurfaceControlCompat.Builder()
+ .setName("testSurfaceControl")
+ .setParent(it.mSurfaceView)
+ .build()
+ SurfaceControlCompat.Transaction()
+ .setExtendedRangeBrightness(surfaceControl, 1.0f, 2.0f)
+ .commit()
+ }
+ }
+ }
+
+ it.addSurface(it.mSurfaceView, callback)
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.S_V2)
+ @Test
+ fun testSetDataSpaceThrowsOnUnsupportedPlatforms() {
+ ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
+ .moveToState(
+ Lifecycle.State.CREATED
+ ).onActivity {
+ val callback = object : SurfaceHolderCallback() {
+ override fun surfaceCreated(sh: SurfaceHolder) {
+
+ assertThrows(UnsupportedOperationException::class.java) {
+ val surfaceControl = SurfaceControlCompat.Builder()
+ .setName("testSurfaceControl")
+ .setParent(it.mSurfaceView)
+ .build()
+
+ val extendedDataspace = DataSpace.pack(
+ DataSpace.STANDARD_BT709,
+ DataSpace.TRANSFER_SRGB, DataSpace.RANGE_EXTENDED
+ )
+ SurfaceControlCompat.Transaction()
+ .setDataSpace(surfaceControl, extendedDataspace)
+ .commit()
+ }
+ }
+ }
+
+ it.addSurface(it.mSurfaceView, callback)
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testSetExtendedRangeBrightness() {
+ val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
+ .moveToState(
+ Lifecycle.State.CREATED
+ ).onActivity {
+ val display = it.display
+ assertNotNull(display)
+ if (display!!.isHdrSdrRatioAvailable) {
+ assertEquals(1.0f, display.hdrSdrRatio, .0001f)
+ }
+
+ it.window.attributes.screenBrightness = 0.01f
+ val hdrReady = CountDownLatch(1)
+ val listenerErrors = arrayOfNulls<Exception>(1)
+ if (display.isHdrSdrRatioAvailable) {
+ display.registerHdrSdrRatioChangedListener(
+ executor!!,
+ object : Consumer<Display?> {
+ var mIsRegistered = true
+ override fun accept(updatedDisplay: Display?) {
+ try {
+ assertEquals(display.displayId, updatedDisplay!!.displayId)
+ assertTrue(mIsRegistered)
+ if (display.hdrSdrRatio > 2f) {
+ hdrReady.countDown()
+ display.unregisterHdrSdrRatioChangedListener(this)
+ mIsRegistered = false
+ }
+ } catch (e: Exception) {
+ synchronized(it) {
+ listenerErrors[0] = e
+ hdrReady.countDown()
+ }
+ }
+ }
+ })
+ } else {
+ assertThrows(IllegalStateException::class.java) {
+ display.registerHdrSdrRatioChangedListener(
+ executor!!,
+ Consumer { _: Display? -> })
+ }
+ }
+ val extendedDataspace = DataSpace.pack(DataSpace.STANDARD_BT709,
+ DataSpace.TRANSFER_SRGB, DataSpace.RANGE_EXTENDED)
+ val buffer = getSolidBuffer(
+ SurfaceControlWrapperTestActivity.DEFAULT_WIDTH,
+ SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT,
+ Color.RED)
+ val callback = object : SurfaceHolderCallback() {
+ override fun surfaceCreated(sh: SurfaceHolder) {
+ val scCompat = SurfaceControlCompat
+ .Builder()
+ .setParent(it.getSurfaceView())
+ .setName("SurfaceControlCompatTest")
+ .build()
+
+ SurfaceControlCompat.Transaction()
+ .setBuffer(scCompat, buffer)
+ .setDataSpace(scCompat, extendedDataspace)
+ .setExtendedRangeBrightness(scCompat, 1.0f, 3.0f)
+ .setVisibility(scCompat, true)
+ .commit()
+ }
+ }
+
+ it.addSurface(it.mSurfaceView, callback)
+ }
+
+ scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+ SurfaceControlUtils.validateOutput(it.window) { bitmap ->
+ val coord = intArrayOf(0, 0)
+ it.mSurfaceView.getLocationInWindow(coord)
+ val topLeft = bitmap.getPixel(
+ coord[0] + 2,
+ coord[1] + 2
+ )
+ val topRight = bitmap.getPixel(
+ coord[0] + it.mSurfaceView.width - 2,
+ coord[1] + 2
+ )
+ val bottomLeft = bitmap.getPixel(
+ coord[0] + 2,
+ coord[1] + it.mSurfaceView.height - 2
+ )
+ val bottomRight = bitmap.getPixel(
+ coord[0] + it.mSurfaceView.width - 2,
+ coord[1] + it.mSurfaceView.height - 2
+ )
+
+ Color.RED == topLeft &&
+ topLeft == topRight &&
+ bottomLeft == topRight &&
+ bottomLeft == bottomRight
+ }
+ }
+ }
+
fun Color.compositeOver(background: Color): Color {
val fg = this.convert(background.colorSpace)
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlUtils.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlUtils.kt
index 91a79e35..ead3b1d 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlUtils.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlUtils.kt
@@ -20,7 +20,10 @@
import android.graphics.Bitmap
import android.graphics.Color
import android.hardware.HardwareBuffer
+import android.os.Build
import android.os.SystemClock
+import android.view.Window
+import androidx.annotation.RequiresApi
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert
@@ -28,6 +31,18 @@
@SdkSuppress(minSdkVersion = 29)
internal class SurfaceControlUtils {
companion object {
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun validateOutput(window: Window, block: (bitmap: Bitmap) -> Boolean) {
+ val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation
+ val bitmap = uiAutomation.takeScreenshot(window)
+ if (bitmap != null) {
+ block(bitmap)
+ } else {
+ throw IllegalArgumentException("Unable to obtain bitmap from screenshot")
+ }
+ }
+
fun validateOutput(block: (bitmap: Bitmap) -> Boolean) {
var sleepDurationMillis = 1000L
var success = false
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferTransformer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferTransformer.kt
index 87b7537..6367e09 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferTransformer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferTransformer.kt
@@ -40,6 +40,9 @@
var glHeight = 0
private set
+ var computedTransform: Int = BufferTransformHintResolver.UNKNOWN_TRANSFORM
+ private set
+
fun invertBufferTransform(transform: Int): Int =
when (transform) {
SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_90 ->
@@ -65,6 +68,7 @@
val fHeight = height.toFloat()
glWidth = width
glHeight = height
+ computedTransform = transformHint
when (transformHint) {
SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_90 -> {
Matrix.setRotateM(mViewTransform, 0, -90f, 0f, 0f, 1f)
@@ -82,8 +86,12 @@
glWidth = height
glHeight = width
}
+ SurfaceControlCompat.BUFFER_TRANSFORM_IDENTITY -> {
+ Matrix.setIdentityM(mViewTransform, 0)
+ }
// Identity or unknown case, just set the identity matrix
else -> {
+ computedTransform = BufferTransformHintResolver.UNKNOWN_TRANSFORM
Matrix.setIdentityM(mViewTransform, 0)
}
}
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRenderer.kt
index 8a66107..ffc799b 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRenderer.kt
@@ -19,8 +19,10 @@
import android.graphics.Canvas
import android.hardware.HardwareBuffer
import android.os.Build
+import androidx.annotation.OptIn
import androidx.annotation.RequiresApi
import androidx.annotation.WorkerThread
+import androidx.core.os.BuildCompat
import androidx.hardware.SyncFenceCompat
import java.util.concurrent.Executor
@@ -28,6 +30,7 @@
* Interface to provide an abstraction around implementations for a low latency hardware
* accelerated [Canvas] that provides a [HardwareBuffer] with the [Canvas] rendered scene
*/
+@RequiresApi(Build.VERSION_CODES.Q)
internal interface SingleBufferedCanvasRenderer<T> {
interface RenderCallbacks<T> {
@@ -67,7 +70,7 @@
companion object {
- @RequiresApi(Build.VERSION_CODES.Q)
+ @OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
fun <T> create(
width: Int,
height: Int,
@@ -75,14 +78,23 @@
executor: Executor,
bufferReadyListener: RenderCallbacks<T>
): SingleBufferedCanvasRenderer<T> {
- // TODO return different instance for corresponding platform version
- return SingleBufferedCanvasRendererV29(
- width,
- height,
- bufferTransformer,
- executor,
- bufferReadyListener
- )
+ return if (BuildCompat.isAtLeastU()) {
+ SingleBufferedCanvasRendererV34(
+ width,
+ height,
+ bufferTransformer,
+ executor,
+ bufferReadyListener
+ )
+ } else {
+ SingleBufferedCanvasRendererV29(
+ width,
+ height,
+ bufferTransformer,
+ executor,
+ bufferReadyListener
+ )
+ }
}
}
}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV34.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV34.kt
new file mode 100644
index 0000000..114a8c2
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV34.kt
@@ -0,0 +1,160 @@
+/*
+ * 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.graphics.lowlatency
+
+import android.graphics.BlendMode
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.HardwareBufferRenderer
+import android.graphics.RenderNode
+import android.hardware.HardwareBuffer
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.SystemClock
+import androidx.annotation.RequiresApi
+import androidx.hardware.SyncFenceCompat
+import java.util.concurrent.Executor
+
+@RequiresApi(34)
+internal class SingleBufferedCanvasRendererV34<T>(
+ private val width: Int,
+ private val height: Int,
+ private val bufferTransformer: BufferTransformer,
+ private val executor: Executor,
+ private val callbacks: SingleBufferedCanvasRenderer.RenderCallbacks<T>
+) : SingleBufferedCanvasRenderer<T> {
+
+ private val mRenderNode = RenderNode("node").apply {
+ setPosition(
+ 0,
+ 0,
+ width,
+ height
+ )
+ clipToBounds = false
+ }
+
+ private val mInverseTransform =
+ bufferTransformer.invertBufferTransform(bufferTransformer.computedTransform)
+ private val mHandlerThread = HandlerThread("renderRequestThread").apply { start() }
+ private val mHandler = Handler(mHandlerThread.looper)
+
+ private inline fun dispatchOnExecutor(crossinline block: () -> Unit) {
+ executor.execute {
+ block()
+ }
+ }
+
+ private inline fun doRender(block: (Canvas) -> Unit) {
+ val canvas = mRenderNode.beginRecording()
+ block(canvas)
+ mRenderNode.endRecording()
+
+ mHardwareBufferRenderer.obtainRenderRequest().apply {
+ if (mInverseTransform != BufferTransformHintResolver.UNKNOWN_TRANSFORM) {
+ setBufferTransform(mInverseTransform)
+ }
+ draw(executor) { result ->
+ callbacks.onBufferReady(mHardwareBuffer, SyncFenceCompat(result.fence))
+ }
+ }
+ }
+
+ private fun tearDown() {
+ mHardwareBufferRenderer.close()
+ mHandlerThread.quit()
+ }
+
+ private val mHardwareBuffer = HardwareBuffer.create(
+ bufferTransformer.glWidth,
+ bufferTransformer.glHeight,
+ HardwareBuffer.RGBA_8888,
+ 1,
+ FrontBufferUtils.obtainHardwareBufferUsageFlags()
+ )
+
+ private val mHardwareBufferRenderer = HardwareBufferRenderer(mHardwareBuffer).apply {
+ setContentRoot(mRenderNode)
+ }
+
+ private var mIsReleasing = false
+
+ override fun render(param: T) {
+ if (!mIsReleasing) {
+ mHandler.post(RENDER) {
+ dispatchOnExecutor {
+ doRender { canvas ->
+ callbacks.render(canvas, width, height, param)
+ }
+ }
+ }
+ }
+ }
+
+ override var isVisible: Boolean = false
+
+ override fun release(cancelPending: Boolean, onReleaseComplete: (() -> Unit)?) {
+ if (!mIsReleasing) {
+ if (cancelPending) {
+ cancelPending()
+ }
+ mHandler.post(RELEASE) {
+ tearDown()
+ if (onReleaseComplete != null) {
+ dispatchOnExecutor {
+ onReleaseComplete.invoke()
+ }
+ }
+ }
+ mIsReleasing = true
+ }
+ }
+
+ override fun clear() {
+ if (!mIsReleasing) {
+ mHandler.post(CLEAR) {
+ dispatchOnExecutor {
+ doRender { canvas ->
+ canvas.drawColor(Color.BLACK, BlendMode.CLEAR)
+ }
+ }
+ }
+ }
+ }
+
+ override fun cancelPending() {
+ if (!mIsReleasing) {
+ mHandler.removeCallbacksAndMessages(CLEAR)
+ mHandler.removeCallbacksAndMessages(RENDER)
+ }
+ }
+
+ private companion object {
+ const val RENDER = 0
+ const val CLEAR = 1
+ const val RELEASE = 2
+ }
+
+ /**
+ * Handler does not expose a post method that takes a token and a runnable.
+ * We need the token to be able to cancel pending requests so just call
+ * postAtTime with the default of SystemClock.uptimeMillis
+ */
+ private fun Handler.post(token: Any?, runnable: Runnable) {
+ postAtTime(runnable, token, SystemClock.uptimeMillis())
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
index daa5636..77ba452 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
@@ -18,12 +18,14 @@
import android.graphics.Rect
import android.graphics.Region
+import android.hardware.DataSpace
import android.hardware.HardwareBuffer
import android.os.Build
import android.view.AttachedSurfaceControl
import android.view.Surface
import android.view.SurfaceControl
import android.view.SurfaceView
+import androidx.annotation.FloatRange
import androidx.annotation.IntDef
import androidx.annotation.RequiresApi
import androidx.graphics.lowlatency.FrontBufferUtils
@@ -476,6 +478,79 @@
}
/**
+ * Sets the desired extended range brightness for the layer. This only applies for layers
+ * that are displaying [HardwareBuffer] instances with a DataSpace of
+ * [DataSpace.RANGE_EXTENDED].
+ *
+ * @param surfaceControl The layer whose extended range brightness is being specified
+ * @param currentBufferRatio The current hdr/sdr ratio of the current buffer. For example
+ * if the buffer was rendered with a target SDR whitepoint of 100 nits and a max display
+ * brightness of 200 nits, this should be set to 2.0f.
+ *
+ * Default value is 1.0f.
+ *
+ * Transfer functions that encode their own brightness ranges,
+ * such as HLG or PQ, should also set this to 1.0f and instead
+ * communicate extended content brightness information via
+ * metadata such as CTA861_3 or SMPTE2086.
+ *
+ * Must be finite && >= 1.0f
+ *
+ * @param desiredRatio The desired hdr/sdr ratio. This can be used to communicate the max
+ * desired brightness range. This is similar to the "max luminance" value in other HDR
+ * metadata formats, but represented as a ratio of the target SDR whitepoint to the max
+ * display brightness. The system may not be able to, or may choose not to, deliver the
+ * requested range.
+ *
+ * While requesting a large desired ratio will result in the most
+ * dynamic range, voluntarily reducing the requested range can help
+ * improve battery life as well as can improve quality by ensuring
+ * greater bit depth is allocated to the luminance range in use.
+ *
+ * Default value is 1.0f and indicates that extended range brightness
+ * is not being used, so the resulting SDR or HDR behavior will be
+ * determined entirely by the dataspace being used (ie, typically SDR
+ * however PQ or HLG transfer functions will still result in HDR)
+ *
+ * Must be finite && >= 1.0f
+ * @return this
+ **/
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun setExtendedRangeBrightness(
+ surfaceControl: SurfaceControlCompat,
+ @FloatRange(from = 1.0, fromInclusive = true) currentBufferRatio: Float,
+ @FloatRange(from = 1.0, fromInclusive = true) desiredRatio: Float
+ ): Transaction {
+ mImpl.setExtendedRangeBrightness(
+ surfaceControl.scImpl,
+ currentBufferRatio,
+ desiredRatio
+ )
+ return this
+ }
+
+ /**
+ * Set the dataspace for the SurfaceControl. This will control how the buffer
+ * set with [setBuffer] is displayed.
+ *
+ * @param surfaceControl The SurfaceControl to update
+ * @param dataSpace The dataspace to set it to. Must be one of named
+ * [android.hardware.DataSpace] types.
+ *
+ * @see [android.view.SurfaceControl.Transaction.setDataSpace]
+ *
+ * @return this
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ fun setDataSpace(
+ surfaceControl: SurfaceControlCompat,
+ dataSpace: Int
+ ): Transaction {
+ mImpl.setDataSpace(surfaceControl.scImpl, dataSpace)
+ return this
+ }
+
+ /**
* Commit the transaction, clearing it's state, and making it usable as a new transaction.
* This will not release any resources and [SurfaceControlCompat.Transaction.close] must be
* called to release the transaction.
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt
index 9d8af62..7e4b0e0 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt
@@ -310,6 +310,25 @@
): Transaction
/**
+ * See [SurfaceControlCompat.Transaction.setExtendedRangeBrightness]
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun setExtendedRangeBrightness(
+ surfaceControl: SurfaceControlImpl,
+ currentBufferRatio: Float,
+ desiredRatio: Float
+ ): Transaction
+
+ /**
+ * See [SurfaceControlCompat.Transaction.setDataSpace]
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ fun setDataSpace(
+ surfaceControl: SurfaceControlImpl,
+ dataSpace: Int
+ ): Transaction
+
+ /**
* Commit the transaction, clearing it's state, and making it usable as a new transaction.
* This will not release any resources and [SurfaceControlImpl.Transaction.close] must be
* called to release the transaction.
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV29.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV29.kt
index c1e1701..13def427 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV29.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV29.kt
@@ -361,6 +361,33 @@
}
/**
+ * See [SurfaceControlCompat.Transaction.setExtendedRangeBrightness]
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ override fun setExtendedRangeBrightness(
+ surfaceControl: SurfaceControlImpl,
+ currentBufferRatio: Float,
+ desiredRatio: Float
+ ): SurfaceControlImpl.Transaction {
+ throw UnsupportedOperationException(
+ "Configuring the extended range brightness is only available on Android U+"
+ )
+ }
+
+ /**
+ * See [SurfaceControlCompat.Transaction.setDataSpace]
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ override fun setDataSpace(
+ surfaceControl: SurfaceControlImpl,
+ dataSpace: Int
+ ): SurfaceControlImpl.Transaction {
+ throw UnsupportedOperationException(
+ "Configuring the data space is only available on Android T+"
+ )
+ }
+
+ /**
* See [SurfaceControlWrapper.Transaction.close]
*/
override fun close() {
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV33.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV33.kt
index 37311f5..2a9e3b0 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV33.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV33.kt
@@ -23,6 +23,7 @@
import android.os.Build
import android.view.AttachedSurfaceControl
import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
import android.view.SurfaceView
import androidx.annotation.RequiresApi
import androidx.hardware.SyncFenceImpl
@@ -259,6 +260,52 @@
}
/**
+ * See [SurfaceControlCompat.Transaction.setExtendedRangeBrightness]
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ override fun setExtendedRangeBrightness(
+ surfaceControl: SurfaceControlImpl,
+ currentBufferRatio: Float,
+ desiredRatio: Float
+ ): SurfaceControlImpl.Transaction {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ SurfaceControlTransactionVerificationHelperV34.setExtendedRangeBrightness(
+ mTransaction,
+ surfaceControl.asFrameworkSurfaceControl(),
+ currentBufferRatio,
+ desiredRatio
+ )
+ return this
+ } else {
+ throw UnsupportedOperationException(
+ "Configuring the extended range brightness is only available on Android U+"
+ )
+ }
+ }
+
+ /**
+ * See [SurfaceControlCompat.Transaction.setDataSpace]
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ override fun setDataSpace(
+ surfaceControl: SurfaceControlImpl,
+ dataSpace: Int
+ ): SurfaceControlImpl.Transaction {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ SurfaceControlTransactionVerificationHelperV33.setDataSpace(
+ mTransaction,
+ surfaceControl.asFrameworkSurfaceControl(),
+ dataSpace
+ )
+ } else {
+ throw UnsupportedOperationException(
+ "Configuring the data space is only available on Android T+"
+ )
+ }
+ return this
+ }
+
+ /**
* See [SurfaceControlImpl.Transaction.commit]
*/
override fun commit() {
@@ -296,4 +343,27 @@
throw IllegalArgumentException("Parent implementation is not for Android T")
}
}
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+private object SurfaceControlTransactionVerificationHelperV34 {
+
+ @androidx.annotation.DoNotInline
+ fun setExtendedRangeBrightness(
+ transaction: Transaction,
+ surfaceControl: SurfaceControl,
+ currentBufferRatio: Float,
+ desiredRatio: Float
+ ) {
+ transaction.setExtendedRangeBrightness(surfaceControl, currentBufferRatio, desiredRatio)
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+private object SurfaceControlTransactionVerificationHelperV33 {
+
+ @androidx.annotation.DoNotInline
+ fun setDataSpace(transaction: Transaction, surfaceControl: SurfaceControl, dataspace: Int) {
+ transaction.setDataSpace(surfaceControl, dataspace)
+ }
}
\ No newline at end of file
diff --git a/graphics/graphics-path/api/current.txt b/graphics/graphics-path/api/current.txt
new file mode 100644
index 0000000..f9570d3
--- /dev/null
+++ b/graphics/graphics-path/api/current.txt
@@ -0,0 +1,33 @@
+// Signature format: 4.0
+package androidx.graphics.path {
+
+ public final class PathSegment {
+ method public android.graphics.PointF![] getPoints();
+ method public androidx.graphics.path.PathSegment.Type getType();
+ method public float getWeight();
+ property public final android.graphics.PointF![] points;
+ property public final androidx.graphics.path.PathSegment.Type type;
+ property public final float weight;
+ }
+
+ public enum PathSegment.Type {
+ method public static androidx.graphics.path.PathSegment.Type valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.graphics.path.PathSegment.Type[] values();
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Close;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Conic;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Cubic;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Done;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Line;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Move;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Quadratic;
+ }
+
+ public final class PathSegmentUtilities {
+ method public static androidx.graphics.path.PathSegment getCloseSegment();
+ method public static androidx.graphics.path.PathSegment getDoneSegment();
+ property public static final androidx.graphics.path.PathSegment CloseSegment;
+ property public static final androidx.graphics.path.PathSegment DoneSegment;
+ }
+
+}
+
diff --git a/graphics/graphics-path/api/public_plus_experimental_current.txt b/graphics/graphics-path/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..35698db
--- /dev/null
+++ b/graphics/graphics-path/api/public_plus_experimental_current.txt
@@ -0,0 +1,61 @@
+// Signature format: 4.0
+package androidx.graphics.path {
+
+ @androidx.core.os.BuildCompat.PrereleaseSdkCheck public final class PathIterator implements java.util.Iterator<androidx.graphics.path.PathSegment> kotlin.jvm.internal.markers.KMappedMarker {
+ ctor public PathIterator(android.graphics.Path path, optional androidx.graphics.path.PathIterator.ConicEvaluation conicEvaluation, optional float tolerance);
+ method public int calculateSize(optional boolean includeConvertedConics);
+ method public androidx.graphics.path.PathIterator.ConicEvaluation getConicEvaluation();
+ method public android.graphics.Path getPath();
+ method public float getTolerance();
+ method public boolean hasNext();
+ method public androidx.graphics.path.PathSegment.Type next(float[] points, optional int offset);
+ method public androidx.graphics.path.PathSegment.Type next(float[] points);
+ method public androidx.graphics.path.PathSegment next();
+ method public androidx.graphics.path.PathSegment.Type peek();
+ property public final androidx.graphics.path.PathIterator.ConicEvaluation conicEvaluation;
+ property public final android.graphics.Path path;
+ property public final float tolerance;
+ }
+
+ public enum PathIterator.ConicEvaluation {
+ method public static androidx.graphics.path.PathIterator.ConicEvaluation valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.graphics.path.PathIterator.ConicEvaluation[] values();
+ enum_constant public static final androidx.graphics.path.PathIterator.ConicEvaluation AsConic;
+ enum_constant public static final androidx.graphics.path.PathIterator.ConicEvaluation AsQuadratics;
+ }
+
+ public final class PathSegment {
+ method public android.graphics.PointF![] getPoints();
+ method public androidx.graphics.path.PathSegment.Type getType();
+ method public float getWeight();
+ property public final android.graphics.PointF![] points;
+ property public final androidx.graphics.path.PathSegment.Type type;
+ property public final float weight;
+ }
+
+ public enum PathSegment.Type {
+ method public static androidx.graphics.path.PathSegment.Type valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.graphics.path.PathSegment.Type[] values();
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Close;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Conic;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Cubic;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Done;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Line;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Move;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Quadratic;
+ }
+
+ public final class PathSegmentUtilities {
+ method public static androidx.graphics.path.PathSegment getCloseSegment();
+ method public static androidx.graphics.path.PathSegment getDoneSegment();
+ property public static final androidx.graphics.path.PathSegment CloseSegment;
+ property public static final androidx.graphics.path.PathSegment DoneSegment;
+ }
+
+ public final class PathUtilities {
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static operator androidx.graphics.path.PathIterator iterator(android.graphics.Path);
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static androidx.graphics.path.PathIterator iterator(android.graphics.Path, androidx.graphics.path.PathIterator.ConicEvaluation conicEvaluation, optional float tolerance);
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/graphics/graphics-path/api/res-current.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to graphics/graphics-path/api/res-current.txt
diff --git a/graphics/graphics-path/api/restricted_current.txt b/graphics/graphics-path/api/restricted_current.txt
new file mode 100644
index 0000000..f9570d3
--- /dev/null
+++ b/graphics/graphics-path/api/restricted_current.txt
@@ -0,0 +1,33 @@
+// Signature format: 4.0
+package androidx.graphics.path {
+
+ public final class PathSegment {
+ method public android.graphics.PointF![] getPoints();
+ method public androidx.graphics.path.PathSegment.Type getType();
+ method public float getWeight();
+ property public final android.graphics.PointF![] points;
+ property public final androidx.graphics.path.PathSegment.Type type;
+ property public final float weight;
+ }
+
+ public enum PathSegment.Type {
+ method public static androidx.graphics.path.PathSegment.Type valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.graphics.path.PathSegment.Type[] values();
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Close;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Conic;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Cubic;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Done;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Line;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Move;
+ enum_constant public static final androidx.graphics.path.PathSegment.Type Quadratic;
+ }
+
+ public final class PathSegmentUtilities {
+ method public static androidx.graphics.path.PathSegment getCloseSegment();
+ method public static androidx.graphics.path.PathSegment getDoneSegment();
+ property public static final androidx.graphics.path.PathSegment CloseSegment;
+ property public static final androidx.graphics.path.PathSegment DoneSegment;
+ }
+
+}
+
diff --git a/graphics/graphics-path/build.gradle b/graphics/graphics-path/build.gradle
new file mode 100644
index 0000000..392c246
--- /dev/null
+++ b/graphics/graphics-path/build.gradle
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+
+ implementation('androidx.appcompat:appcompat:1.6.1')
+ implementation(project(':core:core'))
+
+ androidTestImplementation("androidx.annotation:annotation:1.4.0")
+ androidTestImplementation("androidx.core:core-ktx:1.8.0")
+ androidTestImplementation("androidx.test:core:1.4.0@aar")
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.truth)
+}
+
+android {
+ namespace "androidx.graphics.path"
+
+ defaultConfig {
+ minSdkVersion 21 // Limited to 21+ due to native changes before that release
+ externalNativeBuild {
+ cmake {
+ cppFlags.addAll(
+ [
+ "-std=c++17",
+ "-Wno-unused-command-line-argument",
+ "-Wl,--hash-style=both", // Required to support API levels below 23
+ "-fno-stack-protector",
+ "-fno-exceptions",
+ "-fno-unwind-tables",
+ "-fno-asynchronous-unwind-tables",
+ "-fno-rtti",
+ "-ffast-math",
+ "-ffp-contract=fast",
+ "-fvisibility-inlines-hidden",
+ "-fvisibility=hidden",
+ "-fomit-frame-pointer",
+ "-ffunction-sections",
+ "-fdata-sections",
+ "-Wl,--gc-sections",
+ "-Wl,-Bsymbolic-functions",
+ ])
+ }
+ }
+ }
+
+ externalNativeBuild {
+ cmake {
+ path file('src/main/cpp/CMakeLists.txt')
+ version libs.versions.cmake.get()
+ }
+ }
+
+}
+
+androidx {
+ name = "Android Graphics Path"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenVersion = LibraryVersions.GRAPHICS_PATH
+ inceptionYear = "2022"
+ description = "Query segment data for android.graphics.Path objects"
+}
diff --git a/graphics/graphics-path/src/androidTest/java/androidx/graphics/path/PathIteratorTest.kt b/graphics/graphics-path/src/androidTest/java/androidx/graphics/path/PathIteratorTest.kt
new file mode 100644
index 0000000..5a58802
--- /dev/null
+++ b/graphics/graphics-path/src/androidTest/java/androidx/graphics/path/PathIteratorTest.kt
@@ -0,0 +1,544 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.graphics.path
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.PointF
+import android.graphics.RectF
+import android.os.Build
+import androidx.core.graphics.applyCanvas
+import androidx.core.graphics.createBitmap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import kotlin.math.abs
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private fun assertPointsEquals(p1: PointF, p2: PointF) {
+ assertEquals(p1.x, p2.x, 1e-6f)
+ assertEquals(p1.y, p2.y, 1e-6f)
+}
+
+private fun assertPointsEquals(p1: FloatArray, offset: Int, p2: PointF) {
+ assertEquals(p1[0 + offset * 2], p2.x, 1e-6f)
+ assertEquals(p1[1 + offset * 2], p2.y, 1e-6f)
+}
+
+private fun compareBitmaps(b1: Bitmap, b2: Bitmap) {
+ val epsilon: Int
+ if (Build.VERSION.SDK_INT != 23) {
+ epsilon = 1
+ } else {
+ // There is more AA variability between conics and cubics on API 23, leading
+ // to failures on relatively small visual differences. Increase the error
+ // value for just this release to avoid erroneous bitmap comparison failures.
+ epsilon = 32
+ }
+
+ assertEquals(b1.width, b2.width)
+ assertEquals(b1.height, b2.height)
+
+ val p1 = IntArray(b1.width * b1.height)
+ b1.getPixels(p1, 0, b1.width, 0, 0, b1.width, b1.height)
+
+ val p2 = IntArray(b2.width * b2.height)
+ b2.getPixels(p2, 0, b2.width, 0, 0, b2.width, b2.height)
+
+ for (x in 0 until b1.width) {
+ for (y in 0 until b2.width) {
+ val index = y * b1.width + x
+
+ val c1 = p1[index]
+ val c2 = p2[index]
+
+ assertTrue(abs(Color.red(c1) - Color.red(c2)) <= epsilon)
+ assertTrue(abs(Color.green(c1) - Color.green(c2)) <= epsilon)
+ assertTrue(abs(Color.blue(c1) - Color.blue(c2)) <= epsilon)
+ }
+ }
+}
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PathIteratorTest {
+ @Test
+ fun emptyIterator() {
+ val path = Path()
+
+ val iterator = path.iterator()
+ // TODO: un-comment the hasNext() check when the platform has the behavior change
+ // which ignores final DONE ops in the value for hasNext()
+ // assertFalse(iterator.hasNext())
+ val firstSegment = iterator.next()
+ assertEquals(PathSegment.Type.Done, firstSegment.type)
+
+ var count = 0
+ for (segment in path) {
+ // TODO: remove condition check and just increment count when platform change
+ // is checked in which will not iterate when DONE is the only op left
+ if (segment.type != PathSegment.Type.Done) {
+ // Shouldn't get here; count should remain 0
+ count++
+ }
+ }
+
+ assertEquals(0, count)
+ }
+
+ @Test
+ fun emptyPeek() {
+ val path = Path()
+ val iterator = path.iterator()
+ assertEquals(PathSegment.Type.Done, iterator.peek())
+ }
+
+ @Test
+ fun nonEmptyIterator() {
+ val path = Path().apply {
+ moveTo(1.0f, 1.0f)
+ lineTo(2.0f, 2.0f)
+ close()
+ }
+
+ val iterator = path.iterator()
+ assertTrue(iterator.hasNext())
+
+ val types = arrayOf(
+ PathSegment.Type.Move,
+ PathSegment.Type.Line,
+ PathSegment.Type.Close,
+ PathSegment.Type.Done
+ )
+ val points = arrayOf(
+ PointF(1.0f, 1.0f),
+ PointF(2.0f, 2.0f)
+ )
+
+ var count = 0
+ for (segment in path) {
+ assertEquals(types[count], segment.type)
+ when (segment.type) {
+ PathSegment.Type.Move -> {
+ assertEquals(points[count], segment.points[0])
+ }
+ PathSegment.Type.Line -> {
+ assertEquals(points[count - 1], segment.points[0])
+ assertEquals(points[count], segment.points[1])
+ }
+ else -> { }
+ }
+ // TODO: remove condition and just auto-increment count when platform change is
+ // checked in which ignores DONE during iteration
+ if (segment.type != PathSegment.Type.Done) count++
+ }
+
+ assertEquals(3, count)
+ }
+
+ @Test
+ fun peek() {
+ val path = Path().apply {
+ moveTo(1.0f, 1.0f)
+ lineTo(2.0f, 2.0f)
+ close()
+ }
+
+ val iterator = path.iterator()
+ assertEquals(PathSegment.Type.Move, iterator.peek())
+ }
+
+ @Test
+ fun peekBeyond() {
+ val path = Path()
+ assertEquals(PathSegment.Type.Done, path.iterator().peek())
+
+ path.apply {
+ moveTo(1.0f, 1.0f)
+ lineTo(2.0f, 2.0f)
+ close()
+ }
+
+ val iterator = path.iterator()
+ while (iterator.hasNext()) iterator.next()
+ assertEquals(PathSegment.Type.Done, iterator.peek())
+ }
+
+ @Test
+ fun iteratorStyles() {
+ val path = Path().apply {
+ moveTo(1.0f, 1.0f)
+ lineTo(2.0f, 2.0f)
+ cubicTo(3.0f, 3.0f, 4.0f, 4.0f, 5.0f, 5.0f)
+ quadTo(7.0f, 7.0f, 8.0f, 8.0f)
+ moveTo(10.0f, 10.0f)
+ // addRoundRect() will generate conic curves on certain API levels
+ addRoundRect(RectF(12.0f, 12.0f, 36.0f, 36.0f), 8.0f, 8.0f, Path.Direction.CW)
+ close()
+ }
+
+ iteratorStylesImpl(path, PathIterator.ConicEvaluation.AsConic)
+ iteratorStylesImpl(path, PathIterator.ConicEvaluation.AsQuadratics)
+ }
+
+ private fun iteratorStylesImpl(path: Path, conicEvaluation: PathIterator.ConicEvaluation) {
+ val iterator1 = path.iterator(conicEvaluation)
+ val iterator2 = path.iterator(conicEvaluation)
+ val iterator3 = path.iterator(conicEvaluation)
+
+ val points = FloatArray(8)
+ val points2 = FloatArray(16)
+
+ while (iterator1.hasNext() || iterator2.hasNext() || iterator3.hasNext()) {
+ val segment = iterator1.next()
+ val type = iterator2.next(points)
+ val type2 = iterator3.next(points2, 8)
+
+ assertEquals(type, segment.type)
+ assertEquals(type2, segment.type)
+
+ when (type) {
+ PathSegment.Type.Move -> {
+ assertPointsEquals(points, 0, segment.points[0])
+ assertPointsEquals(points2, 4, segment.points[0])
+ }
+
+ PathSegment.Type.Line -> {
+ assertPointsEquals(points, 0, segment.points[0])
+ assertPointsEquals(points, 1, segment.points[1])
+ assertPointsEquals(points2, 4, segment.points[0])
+ assertPointsEquals(points2, 5, segment.points[1])
+ }
+
+ PathSegment.Type.Quadratic -> {
+ assertPointsEquals(points, 0, segment.points[0])
+ assertPointsEquals(points, 1, segment.points[1])
+ assertPointsEquals(points, 2, segment.points[2])
+ assertPointsEquals(points2, 4, segment.points[0])
+ assertPointsEquals(points2, 5, segment.points[1])
+ assertPointsEquals(points2, 6, segment.points[2])
+ }
+
+ PathSegment.Type.Conic -> {
+ assertPointsEquals(points, 0, segment.points[0])
+ assertPointsEquals(points, 1, segment.points[1])
+ assertPointsEquals(points, 2, segment.points[2])
+ // Weight is stored after all of the points
+ assertEquals(points[6], segment.weight)
+
+ assertPointsEquals(points2, 4, segment.points[0])
+ assertPointsEquals(points2, 5, segment.points[1])
+ assertPointsEquals(points2, 6, segment.points[2])
+ // Weight is stored after all of the points
+ assertEquals(points2[14], segment.weight)
+ }
+
+ PathSegment.Type.Cubic -> {
+ assertPointsEquals(points, 0, segment.points[0])
+ assertPointsEquals(points, 1, segment.points[1])
+ assertPointsEquals(points, 2, segment.points[2])
+ assertPointsEquals(points, 3, segment.points[3])
+
+ assertPointsEquals(points2, 4, segment.points[0])
+ assertPointsEquals(points2, 5, segment.points[1])
+ assertPointsEquals(points2, 6, segment.points[2])
+ assertPointsEquals(points2, 7, segment.points[3])
+ }
+
+ PathSegment.Type.Close -> {}
+ PathSegment.Type.Done -> {}
+ }
+ }
+ }
+
+ @Test
+ fun done() {
+ val path = Path().apply {
+ close()
+ }
+
+ val segment = path.iterator().next()
+
+ assertEquals(PathSegment.Type.Done, segment.type)
+ assertEquals(0, segment.points.size)
+ assertEquals(0.0f, segment.weight)
+ }
+
+ @Test
+ fun close() {
+ val path = Path().apply {
+ lineTo(10.0f, 12.0f)
+ close()
+ }
+
+ val iterator = path.iterator()
+ // Swallow the move
+ iterator.next()
+ // Swallow the line
+ iterator.next()
+
+ val segment = iterator.next()
+
+ assertEquals(PathSegment.Type.Close, segment.type)
+ assertEquals(0, segment.points.size)
+ assertEquals(0.0f, segment.weight)
+ }
+
+ @Test
+ fun moveTo() {
+ val path = Path().apply {
+ moveTo(10.0f, 12.0f)
+ }
+
+ val segment = path.iterator().next()
+
+ assertEquals(PathSegment.Type.Move, segment.type)
+ assertEquals(1, segment.points.size)
+ assertPointsEquals(PointF(10.0f, 12.0f), segment.points[0])
+ assertEquals(0.0f, segment.weight)
+ }
+
+ @Test
+ fun lineTo() {
+ val path = Path().apply {
+ moveTo(4.0f, 6.0f)
+ lineTo(10.0f, 12.0f)
+ }
+
+ val iterator = path.iterator()
+ // Swallow the move
+ iterator.next()
+
+ val segment = iterator.next()
+
+ assertEquals(PathSegment.Type.Line, segment.type)
+ assertEquals(2, segment.points.size)
+ assertPointsEquals(PointF(4.0f, 6.0f), segment.points[0])
+ assertPointsEquals(PointF(10.0f, 12.0f), segment.points[1])
+ assertEquals(0.0f, segment.weight)
+ }
+
+ @Test
+ fun quadraticTo() {
+ val path = Path().apply {
+ moveTo(4.0f, 6.0f)
+ quadTo(10.0f, 12.0f, 20.0f, 24.0f)
+ }
+
+ val iterator = path.iterator()
+ // Swallow the move
+ iterator.next()
+
+ val segment = iterator.next()
+
+ assertEquals(PathSegment.Type.Quadratic, segment.type)
+ assertEquals(3, segment.points.size)
+ assertPointsEquals(PointF(4.0f, 6.0f), segment.points[0])
+ assertPointsEquals(PointF(10.0f, 12.0f), segment.points[1])
+ assertPointsEquals(PointF(20.0f, 24.0f), segment.points[2])
+ assertEquals(0.0f, segment.weight)
+ }
+
+ @Test
+ fun cubicTo() {
+ val path = Path().apply {
+ moveTo(4.0f, 6.0f)
+ cubicTo(10.0f, 12.0f, 20.0f, 24.0f, 30.0f, 36.0f)
+ }
+
+ val iterator = path.iterator()
+ // Swallow the move
+ iterator.next()
+
+ val segment = iterator.next()
+
+ assertEquals(PathSegment.Type.Cubic, segment.type)
+ assertEquals(4, segment.points.size)
+ assertPointsEquals(PointF(4.0f, 6.0f), segment.points[0])
+ assertPointsEquals(PointF(10.0f, 12.0f), segment.points[1])
+ assertPointsEquals(PointF(20.0f, 24.0f), segment.points[2])
+ assertPointsEquals(PointF(30.0f, 36.0f), segment.points[3])
+ assertEquals(0.0f, segment.weight)
+ }
+
+ @Test
+ fun conicTo() {
+ if (Build.VERSION.SDK_INT >= 25) {
+ val path = Path().apply {
+ addRoundRect(RectF(12.0f, 12.0f, 24.0f, 24.0f), 8.0f, 8.0f, Path.Direction.CW)
+ }
+
+ val iterator = path.iterator(PathIterator.ConicEvaluation.AsConic)
+ // Swallow the move
+ iterator.next()
+
+ val segment = iterator.next()
+
+ assertEquals(PathSegment.Type.Conic, segment.type)
+ assertEquals(3, segment.points.size)
+
+ assertPointsEquals(PointF(12.0f, 18.0f), segment.points[0])
+ assertPointsEquals(PointF(12.0f, 12.0f), segment.points[1])
+ assertPointsEquals(PointF(18.0f, 12.0f), segment.points[2])
+ assertEquals(0.70710677f, segment.weight)
+ }
+ }
+
+ @Test
+ fun conicAsQuadratics() {
+ val path = Path().apply {
+ addRoundRect(RectF(12.0f, 12.0f, 24.0f, 24.0f), 8.0f, 8.0f, Path.Direction.CW)
+ }
+
+ for (segment in path) {
+ if (segment.type == PathSegment.Type.Conic) fail("Found conic, none expected: $segment")
+ }
+ }
+
+ @Test
+ fun convertedConics() {
+ val path1 = Path().apply {
+ addRoundRect(RectF(12.0f, 12.0f, 64.0f, 64.0f), 12.0f, 12.0f, Path.Direction.CW)
+ }
+
+ val path2 = Path()
+ for (segment in path1) {
+ when (segment.type) {
+ PathSegment.Type.Move -> path2.moveTo(segment.points[0].x, segment.points[0].y)
+ PathSegment.Type.Line -> path2.lineTo(segment.points[1].x, segment.points[1].y)
+ PathSegment.Type.Quadratic -> path2.quadTo(
+ segment.points[1].x, segment.points[1].y,
+ segment.points[2].x, segment.points[2].y
+ )
+ PathSegment.Type.Conic -> fail("Unexpected conic! $segment")
+ PathSegment.Type.Cubic -> path2.cubicTo(
+ segment.points[1].x, segment.points[1].y,
+ segment.points[2].x, segment.points[2].y,
+ segment.points[3].x, segment.points[3].y
+ )
+ PathSegment.Type.Close -> path2.close()
+ PathSegment.Type.Done -> { }
+ }
+ }
+
+ // Now with smaller error tolerance
+ val path3 = Path()
+ for (segment in path1.iterator(
+ conicEvaluation = PathIterator.ConicEvaluation.AsQuadratics,
+ .001f
+ )) {
+ when (segment.type) {
+ PathSegment.Type.Move -> path3.moveTo(segment.points[0].x, segment.points[0].y)
+ PathSegment.Type.Line -> path3.lineTo(segment.points[1].x, segment.points[1].y)
+ PathSegment.Type.Quadratic -> path3.quadTo(
+ segment.points[1].x, segment.points[1].y,
+ segment.points[2].x, segment.points[2].y
+ )
+ PathSegment.Type.Conic -> fail("Unexpected conic! $segment")
+ PathSegment.Type.Cubic -> path3.cubicTo(
+ segment.points[1].x, segment.points[1].y,
+ segment.points[2].x, segment.points[2].y,
+ segment.points[3].x, segment.points[3].y
+ )
+ PathSegment.Type.Close -> path3.close()
+ PathSegment.Type.Done -> { }
+ }
+ }
+
+ val b1 = createBitmap(76, 76).applyCanvas {
+ drawARGB(255, 255, 255, 255)
+ drawPath(path1, Paint().apply {
+ color = argb(1.0f, 0.0f, 0.0f, 1.0f)
+ strokeWidth = 2.0f
+ isAntiAlias = true
+ style = Paint.Style.STROKE
+ })
+ }
+
+ val b2 = createBitmap(76, 76).applyCanvas {
+ drawARGB(255, 255, 255, 255)
+ drawPath(path2, Paint().apply {
+ color = argb(1.0f, 0.0f, 0.0f, 1.0f)
+ strokeWidth = 2.0f
+ isAntiAlias = true
+ style = Paint.Style.STROKE
+ })
+ }
+
+ compareBitmaps(b1, b2)
+ // Note: b1-vs-b3 is not a valid comparison; default Skia rendering does not use an
+ // error tolerance that low. The test for fine-precision in path3 was just to
+ // ensure that the system could handle the extra data and operations required
+ }
+
+ @Test
+ fun sizes() {
+ val path = Path()
+ var iterator: PathIterator = path.iterator()
+
+ if (iterator.calculateSize() > 0) {
+ assertEquals(PathSegment.Type.Done, iterator.peek())
+ }
+ // TODO: replace above check with below assertEquals after platform change is checked
+ // in which returns a size of zero when there the only op in the path is DONE
+ // assertEquals(0, iterator.size())
+
+ path.addRoundRect(RectF(12.0f, 12.0f, 64.0f, 64.0f), 8.0f, 8.0f,
+ Path.Direction.CW)
+
+ // Skia converted
+ if (Build.VERSION.SDK_INT > 22) {
+ // Preserve conics and count
+ iterator = path.iterator(PathIterator.ConicEvaluation.AsConic)
+ assert(iterator.calculateSize() == 10 || iterator.calculateSize() == 11)
+ // TODO: replace assert() above with assertEquals below once platform change exists
+ // which does not count final DONE in the size
+ // assertEquals(10, iterator.size())
+ assertEquals(iterator.calculateSize(true), iterator.calculateSize())
+ }
+
+ // Convert conics and count
+ iterator = path.iterator(PathIterator.ConicEvaluation.AsQuadratics)
+ if (Build.VERSION.SDK_INT > 22) {
+ // simple size, not including conic conversion
+ assert(iterator.calculateSize(false) == 10 || iterator.calculateSize(false) == 11)
+ // TODO: replace assert() above with assertEquals below once platform change exists
+ // which does not count final DONE in the size
+ // assertEquals(10, iterator.size(false))
+ } else {
+ // round rects pre-API22 used line/quad/quad for each corner
+ assertEquals(14, iterator.calculateSize())
+ }
+ // now get the size with converted conics
+ val size = iterator.calculateSize()
+ assert(size == 14 || size == 15)
+ // TODO: replace assert() above with assertEquals below once platform change exists
+ // which does not count final DONE in the size
+ // assertEquals(14, iterator.size())
+ }
+}
+
+fun argb(alpha: Float, red: Float, green: Float, blue: Float) =
+ ((alpha * 255.0f + 0.5f).toInt() shl 24) or
+ ((red * 255.0f + 0.5f).toInt() shl 16) or
+ ((green * 255.0f + 0.5f).toInt() shl 8) or
+ (blue * 255.0f + 0.5f).toInt()
diff --git a/graphics/graphics-path/src/main/androidx/graphics/androidx-graphics-graphics-path-documentation.md b/graphics/graphics-path/src/main/androidx/graphics/androidx-graphics-graphics-path-documentation.md
new file mode 100644
index 0000000..6c44750
--- /dev/null
+++ b/graphics/graphics-path/src/main/androidx/graphics/androidx-graphics-graphics-path-documentation.md
@@ -0,0 +1,117 @@
+# Package androidx.graphics.paths
+
+Androidx Graphics Path is an Android library that provides new functionalities around the
+[Path](https://developer.android.com/reference/android/graphics/Path) API. Specifically, it
+allows paths to be queried for the segment data they contain,
+
+The library is compatible with API 21+.
+
+## Iterating over a Path
+
+With Pathway you can easily iterate over a `Path` object to inspect its segments
+(curves or commands):
+
+```kotlin
+val path = Path().apply {
+ // Build path content
+}
+
+for (segment in path) {
+ val type = segment.type // The type of segment (move, cubic, quadratic, line, close, etc.)
+ val points = segment.points // The points describing the segment geometry
+}
+```
+
+This type of iteration is easy to use but may create an allocation per segment iterated over.
+If you must avoid allocations, Pathway provides a lower-level API to do so:
+
+```kotlin
+val path = Path().apply {
+ // Build path content
+}
+
+val iterator = path.iterator
+val points = FloatArray(8)
+
+while (iterator.hasNext()) {
+ val type = iterator.next(points) // The type of segment
+ // Read the segment geometry from the points array depending on the type
+}
+
+```
+
+### Path segments
+
+Each segment in a `Path` can be of one of the following types:
+
+#### Move
+
+Move command. The path segment contains 1 point indicating the move destination.
+The weight is set 0.0f and not meaningful.
+
+#### Line
+
+Line curve. The path segment contains 2 points indicating the two extremities of
+the line. The weight is set 0.0f and not meaningful.
+
+#### Quadratic
+
+Quadratic curve. The path segment contains 3 points in the following order:
+- Start point
+- Control point
+- End point
+
+The weight is set 0.0f and not meaningful.
+
+#### Conic
+
+Conic curve. The path segment contains 3 points in the following order:
+- Start point
+- Control point
+- End point
+
+The curve is weighted by the `PathSegment.weight` property.
+
+Conic curves are automatically converted to quadratic curves by default, see
+[Handling conic segments](#handling-conic-segments) below for more information.
+
+#### Cubic
+
+Cubic curve. The path segment contains 4 points in the following order:
+- Start point
+- First control point
+- Second control point
+- End point
+
+The weight is set to 0.0f and is not meaningful.
+
+#### Close
+
+Close command. Close the current contour by joining the last point added to the
+path with the first point of the current contour. The segment does not contain
+any point. The weight is set 0.0f and not meaningful.
+
+#### Done
+
+Done command. This optional command indicates that no further segment will be
+found in the path. It typically indicates the end of an iteration over a path
+and can be ignored.
+
+## Handling conic segments
+
+In some API levels, paths may contain conic curves (weighted quadratics) but the
+`Path` API does not offer a way to add conics to a `Path` object. To work around
+this, Pathway automatically converts conics into several quadratics by default.
+
+The conic to quadratic conversion is an approximation controlled by a tolerance
+threshold, set by default to 0.25f (sub-pixel). If you want to preserve conics
+or control the tolerance, you can use the following APIs:
+
+```kotlin
+// Preserve conics
+val iterator = path.iterator(PathIterator.ConicEvaluation.AsConic)
+
+// Control the tolerance of the conic to quadratic conversion
+val iterator = path.iterator(PathIterator.ConicEvaluation.AsQuadratics, 2.0f)
+
+```
diff --git a/graphics/graphics-path/src/main/cpp/CMakeLists.txt b/graphics/graphics-path/src/main/cpp/CMakeLists.txt
new file mode 100644
index 0000000..e77704f
--- /dev/null
+++ b/graphics/graphics-path/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,20 @@
+cmake_minimum_required(VERSION 3.18.1)
+project("androidx.graphics.path")
+
+add_library(
+ androidx.graphics.path
+ SHARED
+ Conic.cpp
+ PathIterator.cpp
+ pathway.cpp
+)
+
+find_library(
+ log-lib
+ log
+)
+
+target_link_libraries(
+ androidx.graphics.path
+ ${log-lib}
+)
diff --git a/graphics/graphics-path/src/main/cpp/Conic.cpp b/graphics/graphics-path/src/main/cpp/Conic.cpp
new file mode 100644
index 0000000..a6d15b6
--- /dev/null
+++ b/graphics/graphics-path/src/main/cpp/Conic.cpp
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 "Conic.h"
+
+#include "scalar.h"
+
+#include "math/vec2.h"
+
+#include <cmath>
+#include <cstring>
+
+using namespace filament::math;
+
+constexpr int kMaxConicToQuadCount = 5;
+
+constexpr bool isFinite(const Point points[], int count) noexcept {
+ return isFinite(&points[0].x, count << 1);
+}
+
+constexpr bool isFinite(const Point& point) noexcept {
+ float a = 0.0f;
+ a *= point.x;
+ a *= point.y;
+ return a == 0.0f;
+}
+
+constexpr Point toPoint(const float2& v) noexcept {
+ return { .x = v.x, .y = v.y };
+}
+
+constexpr float2 fromPoint(const Point& v) noexcept {
+ return float2{v.x, v.y};
+}
+
+int conicToQuadratics(
+ const Point conicPoints[3], Point *quadraticPoints, int bufferSize,
+ float weight, float tolerance
+) noexcept {
+ Conic conic(conicPoints[0], conicPoints[1], conicPoints[2], weight);
+
+ int count = conic.computeQuadraticCount(tolerance);
+ int quadraticCount = 1 << count;
+ if (quadraticCount > bufferSize) {
+ // Buffer not large enough; return necessary size to resize and try again
+ return quadraticCount;
+ }
+ quadraticCount = conic.splitIntoQuadratics(quadraticPoints, count);
+
+ return quadraticCount;
+}
+
+int Conic::computeQuadraticCount(float tolerance) const noexcept {
+ if (tolerance <= 0.0f || !isFinite(tolerance) || !isFinite(points, 3)) return 0;
+
+ float a = weight - 1.0f;
+ float k = a / (4.0f * (2.0f + a));
+ float x = k * (points[0].x - 2.0f * points[1].x + points[2].x);
+ float y = k * (points[0].y - 2.0f * points[1].y + points[2].y);
+
+ float error = std::sqrtf(x * x + y * y);
+ int count = 0;
+ for ( ; count < kMaxConicToQuadCount; count++) {
+ if (error <= tolerance) break;
+ error *= 0.25f;
+ }
+
+ return count;
+}
+
+static Point* subdivide(const Conic& src, Point pts[], int level) {
+ if (level == 0) {
+ memcpy(pts, &src.points[1], 2 * sizeof(Point));
+ return pts + 2;
+ } else {
+ Conic dst[2];
+ src.split(dst);
+ const float startY = src.points[0].y;
+ const float endY = src.points[2].y;
+ if (between(startY, src.points[1].y, endY)) {
+ float midY = dst[0].points[2].y;
+ if (!between(startY, midY, endY)) {
+ float closerY = tabs(midY - startY) < tabs(midY - endY) ? startY : endY;
+ dst[0].points[2].y = dst[1].points[0].y = closerY;
+ }
+ if (!between(startY, dst[0].points[1].y, dst[0].points[2].y)) {
+ dst[0].points[1].y = startY;
+ }
+ if (!between(dst[1].points[0].y, dst[1].points[1].y, endY)) {
+ dst[1].points[1].y = endY;
+ }
+ }
+ --level;
+ pts = subdivide(dst[0], pts, level);
+ return subdivide(dst[1], pts, level);
+ }
+}
+
+void Conic::split(Conic* __restrict__ dst) const noexcept {
+ float2 scale{1.0f / (1.0f + weight)};
+ float newW = std::sqrtf(0.5f + weight * 0.5f);
+
+ float2 p0 = fromPoint(points[0]);
+ float2 p1 = fromPoint(points[1]);
+ float2 p2 = fromPoint(points[2]);
+ float2 ww(weight);
+
+ float2 wp1 = ww * p1;
+ float2 m = (p0 + (wp1 + wp1) + p2) * scale * float2(0.5f);
+ Point pt = toPoint(m);
+ if (!isFinite(pt)) {
+ double w_d = weight;
+ double w_2 = w_d * 2.0;
+ double scale_half = 1.0 / (1.0 + w_d) * 0.5;
+ pt.x = float((points[0].x + w_2 * points[1].x + points[2].x) * scale_half);
+ pt.y = float((points[0].y + w_2 * points[1].y + points[2].y) * scale_half);
+ }
+ dst[0].points[0] = points[0];
+ dst[0].points[1] = toPoint((p0 + wp1) * scale);
+ dst[0].points[2] = dst[1].points[0] = pt;
+ dst[1].points[1] = toPoint((wp1 + p2) * scale);
+ dst[1].points[2] = points[2];
+
+ dst[0].weight = dst[1].weight = newW;
+}
+
+int Conic::splitIntoQuadratics(Point dstPoints[], int count) const noexcept {
+ *dstPoints = points[0];
+
+ if (count >= kMaxConicToQuadCount) {
+ Conic dst[2];
+ split(dst);
+
+ if (equals(dst[0].points[1], dst[0].points[2]) &&
+ equals(dst[1].points[0], dst[1].points[1])) {
+ dstPoints[1] = dstPoints[2] = dstPoints[3] = dst[0].points[1];
+ dstPoints[4] = dst[1].points[2];
+ count = 1;
+ goto commonFinitePointCheck;
+ }
+ }
+
+ subdivide(*this, dstPoints + 1, count);
+
+commonFinitePointCheck:
+ const int quadCount = 1 << count;
+ const int pointCount = 2 * quadCount + 1;
+
+ if (!isFinite(dstPoints, pointCount)) {
+ for (int i = 1; i < pointCount - 1; ++i) {
+ dstPoints[i] = points[1];
+ }
+ }
+
+ return quadCount;
+}
\ No newline at end of file
diff --git a/graphics/graphics-path/src/main/cpp/Conic.h b/graphics/graphics-path/src/main/cpp/Conic.h
new file mode 100644
index 0000000..548fea2
--- /dev/null
+++ b/graphics/graphics-path/src/main/cpp/Conic.h
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+#ifndef PATH_CONIC_H
+#define PATH_CONIC_H
+
+#include "Path.h"
+
+#include <vector>
+
+constexpr int kDefaultQuadraticCount = 8;
+
+int conicToQuadratics(
+ const Point conicPoints[3], Point *quadraticPoints, int bufferSize,
+ float weight, float tolerance
+) noexcept;
+
+class ConicConverter {
+public:
+ ConicConverter() noexcept { }
+
+private:
+ std::vector<Point> mStorage{1 + 2 * kDefaultQuadraticCount};
+};
+
+struct Conic {
+ Conic() noexcept { }
+
+ Conic(Point p0, Point p1, Point p2, float weight) noexcept {
+ points[0] = p0;
+ points[1] = p1;
+ points[2] = p2;
+ this->weight = weight;
+ }
+
+ void split(Conic* __restrict__ dst) const noexcept;
+ int computeQuadraticCount(float tolerance) const noexcept;
+ int splitIntoQuadratics(Point dstPoints[], int count) const noexcept;
+
+ Point points[3];
+ float weight;
+};
+
+#endif //PATH_CONIC_H
diff --git a/graphics/graphics-path/src/main/cpp/Path.h b/graphics/graphics-path/src/main/cpp/Path.h
new file mode 100644
index 0000000..f25d708
--- /dev/null
+++ b/graphics/graphics-path/src/main/cpp/Path.h
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+
+#ifndef PATH_PATH_H
+#define PATH_PATH_H
+
+#include <stdint.h>
+
+// The following structures declare the minimum we need + a marker (generationId) to
+// validate the data during debugging. There may be more fields in the Skia structures
+// but we just ignore them for now. Some fields declared in older API levels (isFinite
+// for instance) may not show up in the declarations for newer API levels if the field
+// still exist but was moved after the data we need.
+
+enum class Verb : uint8_t {
+ Move,
+ Line,
+ Quadratic,
+ Conic,
+ Cubic,
+ Close,
+ Done
+};
+
+struct Point {
+ float x;
+ float y;
+};
+
+struct PathRef21 {
+ __unused intptr_t pointer; // Virtual tables
+ __unused int32_t refCount;
+ __unused float left;
+ __unused float top;
+ __unused float right;
+ __unused float bottom;
+ __unused uint8_t segmentMask; // Some of the unused fields are in a different order in 22/23
+ __unused uint8_t boundsIsDirty;
+ __unused uint8_t isFinite;
+ __unused uint8_t isOval;
+ Point* points;
+ Verb* verbs;
+ int verbCount;
+ __unused int pointCount;
+ __unused size_t freeSpace;
+ float* conicWeights;
+ __unused int conicWeightsReserve;
+ __unused int conicWeightsCount;
+ __unused uint32_t generationId;
+};
+
+struct PathRef24 {
+ __unused intptr_t pointer;
+ __unused int32_t refCount;
+ __unused float left;
+ __unused float top;
+ __unused float right;
+ __unused float bottom;
+ Point* points;
+ Verb* verbs;
+ int verbCount;
+ __unused int pointCount;
+ __unused size_t freeSpace;
+ float* conicWeights;
+ __unused int conicWeightsReserve;
+ __unused int conicWeightsCount;
+ __unused uint32_t generationId;
+};
+
+struct PathRef26 {
+ __unused int32_t refCount;
+ __unused float left;
+ __unused float top;
+ __unused float right;
+ __unused float bottom;
+ Point* points;
+ Verb* verbs;
+ int verbCount;
+ __unused int pointCount;
+ __unused size_t freeSpace;
+ float* conicWeights;
+ __unused int conicWeightsReserve;
+ __unused int conicWeightsCount;
+ __unused uint32_t generationId;
+};
+
+struct PathRef30 {
+ __unused int32_t refCount;
+ __unused float left;
+ __unused float top;
+ __unused float right;
+ __unused float bottom;
+ Point* points;
+ __unused int pointReserve;
+ __unused int pointCount;
+ Verb* verbs;
+ __unused int verbReserve;
+ int verbCount;
+ float* conicWeights;
+ __unused int conicWeightsReserve;
+ __unused int conicWeightsCount;
+ __unused uint32_t generationId;
+};
+
+struct Path {
+ PathRef21* pathRef;
+};
+
+#endif //PATH_PATH_H
diff --git a/graphics/graphics-path/src/main/cpp/PathIterator.cpp b/graphics/graphics-path/src/main/cpp/PathIterator.cpp
new file mode 100644
index 0000000..a77e251
--- /dev/null
+++ b/graphics/graphics-path/src/main/cpp/PathIterator.cpp
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ */
+
+#include "PathIterator.h"
+
+int PathIterator::count() noexcept {
+ int count = 0;
+ const Verb* verbs = mVerbs;
+ const Point* points = mPoints;
+ const float* conicWeights = mConicWeights;
+
+ for (int i = 0; i < mCount; i++) {
+ Verb verb = *(mDirection == VerbDirection::Forward ? verbs++ : --verbs);
+ switch (verb) {
+ case Verb::Move:
+ case Verb::Line:
+ points += 1;
+ count++;
+ break;
+ case Verb::Quadratic:
+ points += 2;
+ count++;
+ break;
+ case Verb::Conic:
+ points += 2;
+ count++;
+ break;
+ case Verb::Cubic:
+ points += 3;
+ count++;
+ break;
+ case Verb::Close:
+ case Verb::Done:
+ count++;
+ break;
+ }
+ }
+
+ return count;
+}
+
+Verb PathIterator::next(Point points[4]) noexcept {
+ if (mIndex <= 0) {
+ return Verb::Done;
+ }
+ mIndex--;
+
+ Verb verb = *(mDirection == VerbDirection::Forward ? mVerbs++ : --mVerbs);
+ switch (verb) {
+ case Verb::Move:
+ points[0] = mPoints[0];
+ mPoints += 1;
+ break;
+ case Verb::Line:
+ points[0] = mPoints[-1];
+ points[1] = mPoints[0];
+ mPoints += 1;
+ break;
+ case Verb::Quadratic:
+ points[0] = mPoints[-1];
+ points[1] = mPoints[0];
+ points[2] = mPoints[1];
+ mPoints += 2;
+ break;
+ case Verb::Conic:
+ points[0] = mPoints[-1];
+ points[1] = mPoints[0];
+ points[2] = mPoints[1];
+ points[3].x = *mConicWeights;
+ points[3].y = *mConicWeights;
+ mConicWeights++;
+ mPoints += 2;
+ break;
+ case Verb::Cubic:
+ points[0] = mPoints[-1];
+ points[1] = mPoints[0];
+ points[2] = mPoints[1];
+ points[3] = mPoints[2];
+ mPoints += 3;
+ break;
+ case Verb::Close:
+ case Verb::Done:
+ break;
+ }
+
+ return verb;
+}
diff --git a/graphics/graphics-path/src/main/cpp/PathIterator.h b/graphics/graphics-path/src/main/cpp/PathIterator.h
new file mode 100644
index 0000000..f814863
--- /dev/null
+++ b/graphics/graphics-path/src/main/cpp/PathIterator.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef PATH_PATH_ITERATOR_H
+#define PATH_PATH_ITERATOR_H
+
+#include "Path.h"
+#include "Conic.h"
+
+class PathIterator {
+public:
+ enum class VerbDirection : uint8_t {
+ Forward, // API >=30
+ Backward // API < 30
+ };
+
+ PathIterator(
+ Point* points,
+ Verb* verbs,
+ float* conicWeights,
+ int count,
+ VerbDirection direction
+ ) noexcept
+ : mPoints(points),
+ mVerbs(verbs),
+ mConicWeights(conicWeights),
+ mIndex(count),
+ mCount(count),
+ mDirection(direction) {
+ }
+
+ int rawCount() const noexcept { return mCount; }
+
+ int count() noexcept;
+
+ bool hasNext() const noexcept { return mIndex > 0; }
+
+ Verb peek() const noexcept {
+ auto verbs = mDirection == VerbDirection::Forward ? mVerbs : mVerbs - 1;
+ return mIndex > 0 ? *verbs : Verb::Done;
+ }
+
+ Verb next(Point points[4]) noexcept;
+
+private:
+ const Point* mPoints;
+ const Verb* mVerbs;
+ const float* mConicWeights;
+ int mIndex;
+ const int mCount;
+ const VerbDirection mDirection;
+ ConicConverter mConverter;
+};
+
+#endif //PATH_PATH_ITERATOR_H
diff --git a/graphics/graphics-path/src/main/cpp/math/TVecHelpers.h b/graphics/graphics-path/src/main/cpp/math/TVecHelpers.h
new file mode 100644
index 0000000..be00ebd
--- /dev/null
+++ b/graphics/graphics-path/src/main/cpp/math/TVecHelpers.h
@@ -0,0 +1,629 @@
+/*
+ * Copyright 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 TNT_MATH_TVECHELPERS_H
+#define TNT_MATH_TVECHELPERS_H
+
+#include "compiler.h"
+
+#include <cmath> // for std:: namespace
+
+#include <math.h>
+#include <stdint.h>
+#include <sys/types.h>
+
+namespace filament {
+namespace math {
+namespace details {
+// -------------------------------------------------------------------------------------
+
+template<typename U>
+inline constexpr U min(U a, U b) noexcept {
+ return a < b ? a : b;
+}
+
+template<typename U>
+inline constexpr U max(U a, U b) noexcept {
+ return a > b ? a : b;
+}
+
+template<typename T, typename U>
+struct arithmetic_result {
+ using type = decltype(std::declval<T>() + std::declval<U>());
+};
+
+template<typename T, typename U>
+using arithmetic_result_t = typename arithmetic_result<T, U>::type;
+
+template<typename A, typename B = int, typename C = int, typename D = int>
+using enable_if_arithmetic_t = std::enable_if_t<
+ is_arithmetic<A>::value &&
+ is_arithmetic<B>::value &&
+ is_arithmetic<C>::value &&
+ is_arithmetic<D>::value>;
+
+/*
+ * No user serviceable parts here.
+ *
+ * Don't use this file directly, instead include math/vec{2|3|4}.h
+ */
+
+/*
+ * TVec{Add|Product}Operators implements basic arithmetic and basic compound assignments
+ * operators on a vector of type BASE<T>.
+ *
+ * BASE only needs to implement operator[] and size().
+ * By simply inheriting from TVec{Add|Product}Operators<BASE, T> BASE will automatically
+ * get all the functionality here.
+ */
+
+template<template<typename T> class VECTOR, typename T>
+class TVecAddOperators {
+public:
+ /* compound assignment from a another vector of the same size but different
+ * element type.
+ */
+ template<typename U>
+ constexpr VECTOR<T>& operator+=(const VECTOR<U>& v) {
+ VECTOR<T>& lhs = static_cast<VECTOR<T>&>(*this);
+ for (size_t i = 0; i < lhs.size(); i++) {
+ lhs[i] += v[i];
+ }
+ return lhs;
+ }
+
+ template<typename U, typename = enable_if_arithmetic_t<U>>
+ constexpr VECTOR<T>& operator+=(U v) {
+ return operator+=(VECTOR<U>(v));
+ }
+
+ template<typename U>
+ constexpr VECTOR<T>& operator-=(const VECTOR<U>& v) {
+ VECTOR<T>& lhs = static_cast<VECTOR<T>&>(*this);
+ for (size_t i = 0; i < lhs.size(); i++) {
+ lhs[i] -= v[i];
+ }
+ return lhs;
+ }
+
+ template<typename U, typename = enable_if_arithmetic_t<U>>
+ constexpr VECTOR<T>& operator-=(U v) {
+ return operator-=(VECTOR<U>(v));
+ }
+
+private:
+ /*
+ * NOTE: the functions below ARE NOT member methods. They are friend functions
+ * with they definition inlined with their declaration. This makes these
+ * template functions available to the compiler when (and only when) this class
+ * is instantiated, at which point they're only templated on the 2nd parameter
+ * (the first one, BASE<T> being known).
+ */
+
+ template<typename U>
+ friend inline constexpr
+ VECTOR<arithmetic_result_t<T, U>> MATH_PURE operator+(const VECTOR<T>& lv, const VECTOR<U>& rv)
+ {
+ VECTOR<arithmetic_result_t<T, U>> res(lv);
+ res += rv;
+ return res;
+ }
+
+ template<typename U, typename = enable_if_arithmetic_t<U>>
+ friend inline constexpr
+ VECTOR<arithmetic_result_t<T, U>> MATH_PURE operator+(const VECTOR<T>& lv, U rv) {
+ return lv + VECTOR<U>(rv);
+ }
+
+ template<typename U, typename = enable_if_arithmetic_t<U>>
+ friend inline constexpr
+ VECTOR<arithmetic_result_t<T, U>> MATH_PURE operator+(U lv, const VECTOR<T>& rv) {
+ return VECTOR<U>(lv) + rv;
+ }
+
+ template<typename U>
+ friend inline constexpr
+ VECTOR<arithmetic_result_t<T, U>> MATH_PURE operator-(const VECTOR<T>& lv, const VECTOR<U>& rv)
+ {
+ VECTOR<arithmetic_result_t<T, U>> res(lv);
+ res -= rv;
+ return res;
+ }
+
+ template<typename U, typename = enable_if_arithmetic_t<U>>
+ friend inline constexpr
+ VECTOR<arithmetic_result_t<T, U>> MATH_PURE operator-(const VECTOR<T>& lv, U rv) {
+ return lv - VECTOR<U>(rv);
+ }
+
+ template<typename U, typename = enable_if_arithmetic_t<U>>
+ friend inline constexpr
+ VECTOR<arithmetic_result_t<T, U>> MATH_PURE operator-(U lv, const VECTOR<T>& rv) {
+ return VECTOR<U>(lv) - rv;
+ }
+};
+
+template<template<typename T> class VECTOR, typename T>
+class TVecProductOperators {
+public:
+ /* compound assignment from a another vector of the same size but different
+ * element type.
+ */
+ template<typename U>
+ constexpr VECTOR<T>& operator*=(const VECTOR<U>& v) {
+ VECTOR<T>& lhs = static_cast<VECTOR<T>&>(*this);
+ for (size_t i = 0; i < lhs.size(); i++) {
+ lhs[i] *= v[i];
+ }
+ return lhs;
+ }
+
+ template<typename U, typename = enable_if_arithmetic_t<U>>
+ constexpr VECTOR<T>& operator*=(U v) {
+ return operator*=(VECTOR<U>(v));
+ }
+
+ template<typename U>
+ constexpr VECTOR<T>& operator/=(const VECTOR<U>& v) {
+ VECTOR<T>& lhs = static_cast<VECTOR<T>&>(*this);
+ for (size_t i = 0; i < lhs.size(); i++) {
+ lhs[i] /= v[i];
+ }
+ return lhs;
+ }
+
+ template<typename U, typename = enable_if_arithmetic_t<U>>
+ constexpr VECTOR<T>& operator/=(U v) {
+ return operator/=(VECTOR<U>(v));
+ }
+
+private:
+ /*
+ * NOTE: the functions below ARE NOT member methods. They are friend functions
+ * with they definition inlined with their declaration. This makes these
+ * template functions available to the compiler when (and only when) this class
+ * is instantiated, at which point they're only templated on the 2nd parameter
+ * (the first one, BASE<T> being known).
+ */
+
+ template<typename U>
+ friend inline constexpr
+ VECTOR<arithmetic_result_t<T, U>> MATH_PURE operator*(const VECTOR<T>& lv, const VECTOR<U>& rv)
+ {
+ VECTOR<arithmetic_result_t<T, U>> res(lv);
+ res *= rv;
+ return res;
+ }
+
+ template<typename U, typename = enable_if_arithmetic_t<U>>
+ friend inline constexpr
+ VECTOR<arithmetic_result_t<T, U>> MATH_PURE operator*(const VECTOR<T>& lv, U rv) {
+ return lv * VECTOR<U>(rv);
+ }
+
+ template<typename U, typename = enable_if_arithmetic_t<U>>
+ friend inline constexpr
+ VECTOR<arithmetic_result_t<T, U>> MATH_PURE operator*(U lv, const VECTOR<T>& rv) {
+ return VECTOR<U>(lv) * rv;
+ }
+
+ template<typename U>
+ friend inline constexpr
+ VECTOR<arithmetic_result_t<T, U>> MATH_PURE operator/(const VECTOR<T>& lv, const VECTOR<U>& rv)
+ {
+ VECTOR<arithmetic_result_t<T, U>> res(lv);
+ res /= rv;
+ return res;
+ }
+
+ template<typename U, typename = enable_if_arithmetic_t<U>>
+ friend inline constexpr
+ VECTOR<arithmetic_result_t<T, U>> MATH_PURE operator/(const VECTOR<T>& lv, U rv) {
+ return lv / VECTOR<U>(rv);
+ }
+
+ template<typename U, typename = enable_if_arithmetic_t<U>>
+ friend inline constexpr
+ VECTOR<arithmetic_result_t<T, U>> MATH_PURE operator/(U lv, const VECTOR<T>& rv) {
+ return VECTOR<U>(lv) / rv;
+ }
+};
+
+/*
+ * TVecUnaryOperators implements unary operators on a vector of type BASE<T>.
+ *
+ * BASE only needs to implement operator[] and size().
+ * By simply inheriting from TVecUnaryOperators<BASE, T> BASE will automatically
+ * get all the functionality here.
+ *
+ * These operators are implemented as friend functions of TVecUnaryOperators<BASE, T>
+ */
+template<template<typename T> class VECTOR, typename T>
+class TVecUnaryOperators {
+public:
+ constexpr VECTOR<T> operator-() const {
+ VECTOR<T> r{};
+ VECTOR<T> const& rv(static_cast<VECTOR<T> const&>(*this));
+ for (size_t i = 0; i < r.size(); i++) {
+ r[i] = -rv[i];
+ }
+ return r;
+ }
+};
+
+/*
+ * TVecComparisonOperators implements relational/comparison operators
+ * on a vector of type BASE<T>.
+ *
+ * BASE only needs to implement operator[] and size().
+ * By simply inheriting from TVecComparisonOperators<BASE, T> BASE will automatically
+ * get all the functionality here.
+ */
+template<template<typename T> class VECTOR, typename T>
+class TVecComparisonOperators {
+private:
+ /*
+ * NOTE: the functions below ARE NOT member methods. They are friend functions
+ * with they definition inlined with their declaration. This makes these
+ * template functions available to the compiler when (and only when) this class
+ * is instantiated, at which point they're only templated on the 2nd parameter
+ * (the first one, BASE<T> being known).
+ */
+ template<typename U>
+ friend inline constexpr
+ bool MATH_PURE operator==(const VECTOR<T>& lv, const VECTOR<U>& rv) {
+ // w/ inlining we end-up with many branches that will pollute the BPU cache
+ MATH_NOUNROLL
+ for (size_t i = 0; i < lv.size(); i++) {
+ if (lv[i] != rv[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ template<typename U>
+ friend inline constexpr
+ bool MATH_PURE operator!=(const VECTOR<T>& lv, const VECTOR<U>& rv) {
+ return !operator==(lv, rv);
+ }
+
+ template<typename U>
+ friend inline constexpr
+ VECTOR<bool> MATH_PURE equal(const VECTOR<T>& lv, const VECTOR<U>& rv) {
+ VECTOR<bool> r{};
+ for (size_t i = 0; i < lv.size(); i++) {
+ r[i] = lv[i] == rv[i];
+ }
+ return r;
+ }
+
+ template<typename U>
+ friend inline constexpr
+ VECTOR<bool> MATH_PURE notEqual(const VECTOR<T>& lv, const VECTOR<U>& rv) {
+ VECTOR<bool> r{};
+ for (size_t i = 0; i < lv.size(); i++) {
+ r[i] = lv[i] != rv[i];
+ }
+ return r;
+ }
+
+ template<typename U>
+ friend inline constexpr
+ VECTOR<bool> MATH_PURE lessThan(const VECTOR<T>& lv, const VECTOR<U>& rv) {
+ VECTOR<bool> r{};
+ for (size_t i = 0; i < lv.size(); i++) {
+ r[i] = lv[i] < rv[i];
+ }
+ return r;
+ }
+
+ template<typename U>
+ friend inline constexpr
+ VECTOR<bool> MATH_PURE lessThanEqual(const VECTOR<T>& lv, const VECTOR<U>& rv) {
+ VECTOR<bool> r{};
+ for (size_t i = 0; i < lv.size(); i++) {
+ r[i] = lv[i] <= rv[i];
+ }
+ return r;
+ }
+
+ template<typename U>
+ friend inline constexpr
+ VECTOR<bool> MATH_PURE greaterThan(const VECTOR<T>& lv, const VECTOR<U>& rv) {
+ VECTOR<bool> r;
+ for (size_t i = 0; i < lv.size(); i++) {
+ r[i] = lv[i] > rv[i];
+ }
+ return r;
+ }
+
+ template<typename U>
+ friend inline
+ VECTOR<bool> MATH_PURE greaterThanEqual(const VECTOR<T>& lv, const VECTOR<U>& rv) {
+ VECTOR<bool> r{};
+ for (size_t i = 0; i < lv.size(); i++) {
+ r[i] = lv[i] >= rv[i];
+ }
+ return r;
+ }
+};
+
+/*
+ * TVecFunctions implements functions on a vector of type BASE<T>.
+ *
+ * BASE only needs to implement operator[] and size().
+ * By simply inheriting from TVecFunctions<BASE, T> BASE will automatically
+ * get all the functionality here.
+ */
+template<template<typename T> class VECTOR, typename T>
+class TVecFunctions {
+private:
+ /*
+ * NOTE: the functions below ARE NOT member methods. They are friend functions
+ * with they definition inlined with their declaration. This makes these
+ * template functions available to the compiler when (and only when) this class
+ * is instantiated, at which point they're only templated on the 2nd parameter
+ * (the first one, BASE<T> being known).
+ */
+ template<typename U>
+ friend constexpr inline
+ arithmetic_result_t<T, U> MATH_PURE dot(const VECTOR<T>& lv, const VECTOR<U>& rv) {
+ arithmetic_result_t<T, U> r{};
+ for (size_t i = 0; i < lv.size(); i++) {
+ r += lv[i] * rv[i];
+ }
+ return r;
+ }
+
+ friend inline T MATH_PURE norm(const VECTOR<T>& lv) {
+ return std::sqrt(dot(lv, lv));
+ }
+
+ friend inline T MATH_PURE length(const VECTOR<T>& lv) {
+ return norm(lv);
+ }
+
+ friend inline constexpr T MATH_PURE norm2(const VECTOR<T>& lv) {
+ return dot(lv, lv);
+ }
+
+ friend inline constexpr T MATH_PURE length2(const VECTOR<T>& lv) {
+ return norm2(lv);
+ }
+
+ template<typename U>
+ friend inline constexpr
+ arithmetic_result_t<T, U> MATH_PURE distance(const VECTOR<T>& lv, const VECTOR<U>& rv) {
+ return length(rv - lv);
+ }
+
+ template<typename U>
+ friend inline constexpr
+ arithmetic_result_t<T, U> MATH_PURE distance2(const VECTOR<T>& lv, const VECTOR<U>& rv) {
+ return length2(rv - lv);
+ }
+
+ friend inline VECTOR<T> MATH_PURE normalize(const VECTOR<T>& lv) {
+ return lv * (T(1) / length(lv));
+ }
+
+ friend inline VECTOR<T> MATH_PURE rcp(VECTOR<T> v) {
+ return T(1) / v;
+ }
+
+ friend inline constexpr VECTOR<T> MATH_PURE abs(VECTOR<T> v) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = v[i] < 0 ? -v[i] : v[i];
+ }
+ return v;
+ }
+
+ friend inline VECTOR<T> MATH_PURE floor(VECTOR<T> v) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = std::floor(v[i]);
+ }
+ return v;
+ }
+
+ friend inline VECTOR<T> MATH_PURE ceil(VECTOR<T> v) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = std::ceil(v[i]);
+ }
+ return v;
+ }
+
+ friend inline VECTOR<T> MATH_PURE round(VECTOR<T> v) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = std::round(v[i]);
+ }
+ return v;
+ }
+
+ friend inline VECTOR<T> MATH_PURE inversesqrt(VECTOR<T> v) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = T(1) / std::sqrt(v[i]);
+ }
+ return v;
+ }
+
+ friend inline VECTOR<T> MATH_PURE sqrt(VECTOR<T> v) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = std::sqrt(v[i]);
+ }
+ return v;
+ }
+
+ friend inline VECTOR<T> MATH_PURE cbrt(VECTOR<T> v) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = std::cbrt(v[i]);
+ }
+ return v;
+ }
+
+ friend inline VECTOR<T> MATH_PURE exp(VECTOR<T> v) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = std::exp(v[i]);
+ }
+ return v;
+ }
+
+ friend inline VECTOR<T> MATH_PURE pow(VECTOR<T> v, T p) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = std::pow(v[i], p);
+ }
+ return v;
+ }
+
+ friend inline VECTOR<T> MATH_PURE pow(T v, VECTOR<T> p) {
+ for (size_t i = 0; i < p.size(); i++) {
+ p[i] = std::pow(v, p[i]);
+ }
+ return p;
+ }
+
+ friend inline VECTOR<T> MATH_PURE pow(VECTOR<T> v, VECTOR<T> p) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = std::pow(v[i], p[i]);
+ }
+ return v;
+ }
+
+ friend inline VECTOR<T> MATH_PURE log(VECTOR<T> v) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = std::log(v[i]);
+ }
+ return v;
+ }
+
+ friend inline VECTOR<T> MATH_PURE log10(VECTOR<T> v) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = std::log10(v[i]);
+ }
+ return v;
+ }
+
+ friend inline VECTOR<T> MATH_PURE log2(VECTOR<T> v) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = std::log2(v[i]);
+ }
+ return v;
+ }
+
+ friend inline constexpr VECTOR<T> MATH_PURE saturate(const VECTOR<T>& lv) {
+ return clamp(lv, T(0), T(1));
+ }
+
+ friend inline constexpr VECTOR<T> MATH_PURE clamp(VECTOR<T> v, T min, T max) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = details::min(max, details::max(min, v[i]));
+ }
+ return v;
+ }
+
+ friend inline constexpr VECTOR<T> MATH_PURE clamp(VECTOR<T> v, VECTOR<T> min, VECTOR<T> max) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = details::min(max[i], details::max(min[i], v[i]));
+ }
+ return v;
+ }
+
+ friend inline constexpr VECTOR<T> MATH_PURE fma(const VECTOR<T>& lv, const VECTOR<T>& rv,
+ VECTOR<T> a) {
+ for (size_t i = 0; i < lv.size(); i++) {
+ a[i] += (lv[i] * rv[i]);
+ }
+ return a;
+ }
+
+ friend inline constexpr VECTOR<T> MATH_PURE min(const VECTOR<T>& u, VECTOR<T> v) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = details::min(u[i], v[i]);
+ }
+ return v;
+ }
+
+ friend inline constexpr VECTOR<T> MATH_PURE max(const VECTOR<T>& u, VECTOR<T> v) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = details::max(u[i], v[i]);
+ }
+ return v;
+ }
+
+ friend inline constexpr T MATH_PURE max(const VECTOR<T>& v) {
+ T r(v[0]);
+ for (size_t i = 1; i < v.size(); i++) {
+ r = max(r, v[i]);
+ }
+ return r;
+ }
+
+ friend inline constexpr T MATH_PURE min(const VECTOR<T>& v) {
+ T r(v[0]);
+ for (size_t i = 1; i < v.size(); i++) {
+ r = min(r, v[i]);
+ }
+ return r;
+ }
+
+ friend inline constexpr VECTOR<T> MATH_PURE mix(const VECTOR<T>& u, VECTOR<T> v, T a) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = u[i] * (T(1) - a) + v[i] * a;
+ }
+ return v;
+ }
+
+ friend inline constexpr VECTOR<T> MATH_PURE smoothstep(T edge0, T edge1, VECTOR<T> v) {
+ VECTOR<T> t = saturate((v - edge0) / (edge1 - edge0));
+ return t * t * (T(3) - T(2) * t);
+ }
+
+ friend inline constexpr VECTOR<T> MATH_PURE step(T edge, VECTOR<T> v) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = v[i] < edge ? T(0) : T(1);
+ }
+ return v;
+ }
+
+ friend inline constexpr VECTOR<T> MATH_PURE step(VECTOR<T> edge, VECTOR<T> v) {
+ for (size_t i = 0; i < v.size(); i++) {
+ v[i] = v[i] < edge[i] ? T(0) : T(1);
+ }
+ return v;
+ }
+
+ friend inline constexpr bool MATH_PURE any(const VECTOR<T>& v) {
+ for (size_t i = 0; i < v.size(); i++) {
+ if (v[i] != T(0)) return true;
+ }
+ return false;
+ }
+
+ friend inline constexpr bool MATH_PURE all(const VECTOR<T>& v) {
+ bool result = true;
+ for (size_t i = 0; i < v.size(); i++) {
+ result &= (v[i] != T(0));
+ }
+ return result;
+ }
+};
+
+// -------------------------------------------------------------------------------------
+} // namespace details
+} // namespace math
+} // namespace filament
+
+#endif // TNT_MATH_TVECHELPERS_H
diff --git a/graphics/graphics-path/src/main/cpp/math/compiler.h b/graphics/graphics-path/src/main/cpp/math/compiler.h
new file mode 100644
index 0000000..d6e18aa
--- /dev/null
+++ b/graphics/graphics-path/src/main/cpp/math/compiler.h
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 PATH_MATH_COMPILER_H
+#define PATH_MATH_COMPILER_H
+
+#include <type_traits>
+
+#if defined (WIN32)
+
+#ifdef max
+#undef max
+#endif
+
+#ifdef min
+#undef min
+#endif
+
+#ifdef far
+#undef far
+#endif
+
+#ifdef near
+#undef near
+#endif
+
+#endif
+
+// compatibility with non-clang compilers...
+#ifndef __has_attribute
+#define __has_attribute(x) 0
+#endif
+#ifndef __has_builtin
+#define __has_builtin(x) 0
+#endif
+
+#if __has_builtin(__builtin_expect)
+# ifdef __cplusplus
+# define MATH_LIKELY( exp ) (__builtin_expect( !!(exp), true ))
+# define MATH_UNLIKELY( exp ) (__builtin_expect( !!(exp), false ))
+# else
+# define MATH_LIKELY( exp ) (__builtin_expect( !!(exp), 1 ))
+# define MATH_UNLIKELY( exp ) (__builtin_expect( !!(exp), 0 ))
+# endif
+#else
+# define MATH_LIKELY( exp ) (exp)
+# define MATH_UNLIKELY( exp ) (exp)
+#endif
+
+#if __has_attribute(unused)
+# define MATH_UNUSED __attribute__((unused))
+#else
+# define MATH_UNUSED
+#endif
+
+#if __has_attribute(pure)
+# define MATH_PURE __attribute__((pure))
+#else
+# define MATH_PURE
+#endif
+
+#ifdef _MSC_VER
+# define MATH_EMPTY_BASES __declspec(empty_bases)
+
+// MSVC does not support loop unrolling hints
+# define MATH_NOUNROLL
+
+// Sadly, MSVC does not support __builtin_constant_p
+# ifndef MAKE_CONSTEXPR
+# define MAKE_CONSTEXPR(e) (e)
+# endif
+
+// About value initialization, the C++ standard says:
+// if T is a class type with a default constructor that is neither user-provided nor deleted
+// (that is, it may be a class with an implicitly-defined or defaulted default constructor),
+// the object is zero-initialized and then it is default-initialized
+// if it has a non-trivial default constructor;
+// Unfortunately, MSVC always calls the default constructor, even if it is trivial, which
+// breaks constexpr-ness. To workaround this, we're always zero-initializing TVecN<>
+# define MATH_CONSTEXPR_INIT {}
+# define MATH_DEFAULT_CTOR {}
+# define MATH_DEFAULT_CTOR_CONSTEXPR constexpr
+# define CONSTEXPR_IF_NOT_MSVC // when declared constexpr, msvc fails with
+ // "failure was caused by cast of object of dynamic type"
+
+#else // _MSC_VER
+
+# define MATH_EMPTY_BASES
+// C++11 allows pragmas to be specified as part of defines using the _Pragma syntax.
+# define MATH_NOUNROLL _Pragma("nounroll")
+
+# ifndef MAKE_CONSTEXPR
+# define MAKE_CONSTEXPR(e) __builtin_constant_p(e) ? (e) : (e)
+# endif
+
+# define MATH_CONSTEXPR_INIT
+# define MATH_DEFAULT_CTOR = default;
+# define MATH_DEFAULT_CTOR_CONSTEXPR
+# define CONSTEXPR_IF_NOT_MSVC constexpr
+
+#endif // _MSC_VER
+
+namespace filament::math {
+
+// MSVC 2019 16.4 doesn't seem to like it when we specialize std::is_arithmetic for
+// filament::math::half, so we're forced to create our own is_arithmetic here and specialize it
+// inside of half.h.
+template<typename T>
+struct is_arithmetic : std::integral_constant<bool,
+ std::is_integral<T>::value || std::is_floating_point<T>::value> {
+};
+
+} // filament::math
+
+#endif // PATH_MATH_COMPILER_H
diff --git a/graphics/graphics-path/src/main/cpp/math/vec2.h b/graphics/graphics-path/src/main/cpp/math/vec2.h
new file mode 100644
index 0000000..3228b09
--- /dev/null
+++ b/graphics/graphics-path/src/main/cpp/math/vec2.h
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 TNT_MATH_VEC2_H
+#define TNT_MATH_VEC2_H
+
+#include "TVecHelpers.h"
+
+#include <type_traits>
+
+#include <assert.h>
+#include <stdint.h>
+#include <sys/types.h>
+
+namespace filament {
+namespace math {
+// -------------------------------------------------------------------------------------
+
+namespace details {
+
+template<typename T>
+class MATH_EMPTY_BASES TVec2 :
+ public TVecProductOperators<TVec2, T>,
+ public TVecAddOperators<TVec2, T>,
+ public TVecUnaryOperators<TVec2, T>,
+ public TVecComparisonOperators<TVec2, T>,
+ public TVecFunctions<TVec2, T> {
+public:
+ typedef T value_type;
+ typedef T& reference;
+ typedef T const& const_reference;
+ typedef size_t size_type;
+ static constexpr size_t SIZE = 2;
+
+ union {
+ T v[SIZE] MATH_CONSTEXPR_INIT;
+ struct { T x, y; };
+ struct { T s, t; };
+ struct { T r, g; };
+ };
+
+ inline constexpr size_type size() const { return SIZE; }
+
+ // array access
+ inline constexpr T const& operator[](size_t i) const noexcept {
+ assert(i < SIZE);
+ return v[i];
+ }
+
+ inline constexpr T& operator[](size_t i) noexcept {
+ assert(i < SIZE);
+ return v[i];
+ }
+
+ // constructors
+
+ // default constructor
+ MATH_DEFAULT_CTOR_CONSTEXPR TVec2() MATH_DEFAULT_CTOR
+
+ // handles implicit conversion to a tvec4. must not be explicit.
+ template<typename A, typename = enable_if_arithmetic_t<A>>
+ constexpr TVec2(A v) noexcept : v{ T(v), T(v) } {}
+
+ template<typename A, typename B, typename = enable_if_arithmetic_t<A, B>>
+ constexpr TVec2(A x, B y) noexcept : v{ T(x), T(y) } {}
+
+ template<typename A, typename = enable_if_arithmetic_t<A>>
+ constexpr TVec2(const TVec2<A>& v) noexcept : v{ T(v[0]), T(v[1]) } {}
+
+ // cross product works only on vectors of size 2 or 3
+ template<typename U>
+ friend inline constexpr
+ arithmetic_result_t<T, U> cross(const TVec2& u, const TVec2<U>& v) noexcept {
+ return u[0] * v[1] - u[1] * v[0];
+ }
+};
+
+} // namespace details
+
+// ----------------------------------------------------------------------------------------
+
+template<typename T, typename = details::enable_if_arithmetic_t<T>>
+using vec2 = details::TVec2<T>;
+
+using double2 = vec2<double>;
+using float2 = vec2<float>;
+using int2 = vec2<int32_t>;
+using uint2 = vec2<uint32_t>;
+using short2 = vec2<int16_t>;
+using ushort2 = vec2<uint16_t>;
+using byte2 = vec2<int8_t>;
+using ubyte2 = vec2<uint8_t>;
+using bool2 = vec2<bool>;
+
+// ----------------------------------------------------------------------------------------
+} // namespace math
+} // namespace filament
+
+#endif // TNT_MATH_VEC2_H
diff --git a/graphics/graphics-path/src/main/cpp/pathway.cpp b/graphics/graphics-path/src/main/cpp/pathway.cpp
new file mode 100644
index 0000000..9997387
--- /dev/null
+++ b/graphics/graphics-path/src/main/cpp/pathway.cpp
@@ -0,0 +1,229 @@
+/*
+ * 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.
+ */
+
+#include "PathIterator.h"
+
+#include <jni.h>
+
+#include <sys/system_properties.h>
+
+#include <mutex>
+
+#define JNI_CLASS_NAME "androidx/graphics/path/PathIteratorPreApi34Impl"
+#define JNI_CLASS_NAME_CONVERTER "androidx/graphics/path/ConicConverter"
+
+#if !defined(NDEBUG)
+#include <android/log.h>
+#define ANDROID_LOG_TAG "PathIterator"
+#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, ANDROID_LOG_TAG, __VA_ARGS__)
+#endif
+
+struct {
+ jclass jniClass;
+ jfieldID nativePath;
+} sPath{};
+
+uint32_t sApiLevel = 0;
+std::once_flag sApiLevelOnceFlag;
+
+static uint32_t api_level() {
+ std::call_once(sApiLevelOnceFlag, []() {
+ char sdkVersion[PROP_VALUE_MAX];
+ __system_property_get("ro.build.version.sdk", sdkVersion);
+ sApiLevel = atoi(sdkVersion); // NOLINT(cert-err34-c)
+ });
+ return sApiLevel;
+}
+
+static jlong createPathIterator(JNIEnv* env, jobject,
+ jobject path_, jint conicEvaluation_, jfloat tolerance_) {
+
+ auto nativePath = static_cast<intptr_t>(env->GetLongField(path_, sPath.nativePath));
+ auto* path = reinterpret_cast<Path*>(nativePath);
+
+ Point* points;
+ Verb* verbs;
+ float* conicWeights;
+ int count;
+ PathIterator::VerbDirection direction;
+
+ const uint32_t apiLevel = api_level();
+ if (apiLevel >= 30) {
+ auto* ref = reinterpret_cast<PathRef30*>(path->pathRef);
+ points = ref->points;
+ verbs = ref->verbs;
+ conicWeights = ref->conicWeights;
+ count = ref->verbCount;
+ direction = PathIterator::VerbDirection::Forward;
+ } else if (apiLevel >= 26) {
+ auto* ref = reinterpret_cast<PathRef26*>(path->pathRef);
+ points = ref->points;
+ verbs = ref->verbs;
+ conicWeights = ref->conicWeights;
+ count = ref->verbCount;
+ direction = PathIterator::VerbDirection::Backward;
+ } else if (apiLevel >= 24) {
+ auto* ref = reinterpret_cast<PathRef24*>(path->pathRef);
+ points = ref->points;
+ verbs = ref->verbs;
+ conicWeights = ref->conicWeights;
+ count = ref->verbCount;
+ direction = PathIterator::VerbDirection::Backward;
+ } else {
+ auto* ref = path->pathRef;
+ points = ref->points;
+ verbs = ref->verbs;
+ conicWeights = ref->conicWeights;
+ count = ref->verbCount;
+ direction = PathIterator::VerbDirection::Backward;
+ }
+
+ return jlong(new PathIterator(points, verbs, conicWeights, count, direction));
+}
+
+static void destroyPathIterator(JNIEnv*, jobject, jlong pathIterator_) {
+ delete reinterpret_cast<PathIterator*>(pathIterator_);
+}
+
+static jboolean pathIteratorHasNext(JNIEnv*, jobject, jlong pathIterator_) {
+ return reinterpret_cast<PathIterator*>(pathIterator_)->hasNext();
+}
+
+static jint conicToQuadraticsWrapper(JNIEnv* env, jobject,
+ jfloatArray conicPoints, jfloatArray quadraticPoints,
+ jfloat weight, jfloat tolerance, jint offset) {
+ float *conicData1 = env->GetFloatArrayElements(conicPoints, JNI_FALSE);
+ float *quadData1 = env->GetFloatArrayElements(quadraticPoints, JNI_FALSE);
+ int quadDataSize = env->GetArrayLength(quadraticPoints);
+
+ int count = conicToQuadratics(reinterpret_cast<Point *>(conicData1 + offset),
+ reinterpret_cast<Point *>(quadData1),
+ env->GetArrayLength(quadraticPoints),
+ weight, tolerance);
+
+ env->ReleaseFloatArrayElements(conicPoints, conicData1, 0);
+ env->ReleaseFloatArrayElements(quadraticPoints, quadData1, 0);
+
+ return count;
+}
+
+static jint pathIteratorNext(JNIEnv* env, jobject,
+ jlong pathIterator_, jfloatArray points_, jint offset_) {
+ auto pathIterator = reinterpret_cast<PathIterator*>(pathIterator_);
+ Point pointsData[4];
+ Verb verb = pathIterator->next(pointsData);
+
+ if (verb != Verb::Done && verb != Verb::Close) {
+ auto* floatsData = reinterpret_cast<jfloat*>(pointsData);
+ env->SetFloatArrayRegion(points_, offset_, 8, floatsData);
+ }
+
+ return static_cast<jint>(verb);
+}
+
+static jint pathIteratorPeek(JNIEnv*, jobject, jlong pathIterator_) {
+ return static_cast<jint>(reinterpret_cast<PathIterator *>(pathIterator_)->peek());
+}
+
+static jint pathIteratorRawSize(JNIEnv*, jobject, jlong pathIterator_) {
+ return static_cast<jint>(reinterpret_cast<PathIterator *>(pathIterator_)->rawCount());
+}
+
+static jint pathIteratorSize(JNIEnv*, jobject, jlong pathIterator_) {
+ return static_cast<jint>(reinterpret_cast<PathIterator *>(pathIterator_)->count());
+}
+
+JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) {
+ JNIEnv* env;
+ if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ return -1;
+ }
+
+ sPath.jniClass = env->FindClass("android/graphics/Path");
+ if (sPath.jniClass == nullptr) return JNI_ERR;
+
+ sPath.nativePath = env->GetFieldID(sPath.jniClass, "mNativePath", "J");
+ if (sPath.nativePath == nullptr) return JNI_ERR;
+
+ {
+ jclass pathsClass = env->FindClass(JNI_CLASS_NAME);
+ if (pathsClass == nullptr) return JNI_ERR;
+
+ static const JNINativeMethod methods[] = {
+ {
+ (char*) "createInternalPathIterator",
+ (char*) "(Landroid/graphics/Path;IF)J",
+ reinterpret_cast<void*>(createPathIterator)
+ },
+ {
+ (char*) "destroyInternalPathIterator",
+ (char*) "(J)V",
+ reinterpret_cast<void*>(destroyPathIterator)
+ },
+ {
+ (char*) "internalPathIteratorHasNext",
+ (char*) "(J)Z",
+ reinterpret_cast<void*>(pathIteratorHasNext)
+ },
+ {
+ (char*) "internalPathIteratorNext",
+ (char*) "(J[FI)I",
+ reinterpret_cast<void*>(pathIteratorNext)
+ },
+ {
+ (char*) "internalPathIteratorPeek",
+ (char*) "(J)I",
+ reinterpret_cast<void*>(pathIteratorPeek)
+ },
+ {
+ (char*) "internalPathIteratorRawSize",
+ (char*) "(J)I",
+ reinterpret_cast<void*>(pathIteratorRawSize)
+ },
+ {
+ (char*) "internalPathIteratorSize",
+ (char*) "(J)I",
+ reinterpret_cast<void*>(pathIteratorSize)
+ },
+ };
+
+ int result = env->RegisterNatives(
+ pathsClass, methods, sizeof(methods) / sizeof(JNINativeMethod)
+ );
+ if (result != JNI_OK) return result;
+
+ env->DeleteLocalRef(pathsClass);
+
+ jclass converterClass = env->FindClass(JNI_CLASS_NAME_CONVERTER);
+ if (converterClass == nullptr) return JNI_ERR;
+ static const JNINativeMethod methods2[] = {
+ {
+ (char *) "internalConicToQuadratics",
+ (char *) "([F[FFFI)I",
+ reinterpret_cast<void *>(conicToQuadraticsWrapper)
+ },
+ };
+
+ result = env->RegisterNatives(
+ converterClass, methods2, sizeof(methods2) / sizeof(JNINativeMethod)
+ );
+ if (result != JNI_OK) return result;
+
+ env->DeleteLocalRef(converterClass);
+ }
+
+ return JNI_VERSION_1_6;
+}
diff --git a/graphics/graphics-path/src/main/cpp/scalar.h b/graphics/graphics-path/src/main/cpp/scalar.h
new file mode 100644
index 0000000..0342d13
--- /dev/null
+++ b/graphics/graphics-path/src/main/cpp/scalar.h
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+#ifndef PATH_SCALAR_H
+#define PATH_SCALAR_H
+
+union floatIntUnion {
+ float value;
+ int32_t signBitInt;
+};
+
+static inline int32_t float2Bits(float x) noexcept {
+ floatIntUnion data; // NOLINT(cppcoreguidelines-pro-type-member-init)
+ data.value = x;
+ return data.signBitInt;
+}
+
+constexpr bool isFloatFinite(int32_t bits) noexcept {
+ constexpr int32_t kFloatBitsExponentMask = 0x7F800000;
+ return (bits & kFloatBitsExponentMask) != kFloatBitsExponentMask;
+}
+
+static inline bool isFinite(float v) noexcept {
+ return isFloatFinite(float2Bits(v));
+}
+
+#pragma clang diagnostic push
+#pragma ide diagnostic ignored "cppcoreguidelines-narrowing-conversions"
+static bool canNormalize(float dx, float dy) noexcept {
+ return (isFinite(dx) && isFinite(dy)) && (dx || dy);
+}
+#pragma clang diagnostic pop
+
+static bool equals(const Point& p1, const Point& p2) noexcept {
+ return !canNormalize(p1.x - p2.x, p1.y - p2.y);
+}
+
+constexpr bool isFinite(const float array[], int count) noexcept {
+ float prod = 0.0f;
+ for (int i = 0; i < count; i++) {
+ prod *= array[i];
+ }
+ return prod == 0.0f;
+}
+
+template<typename T>
+constexpr T tabs(T value) noexcept {
+ if (value < 0) {
+ value = -value;
+ }
+ return value;
+}
+
+constexpr bool between(float a, float b, float c) noexcept {
+ return (a - b) * (c - b) <= 0.0f;
+}
+
+#endif //PATH_SCALAR_H
diff --git a/graphics/graphics-path/src/main/java/androidx/graphics/path/ConicConverter.kt b/graphics/graphics-path/src/main/java/androidx/graphics/path/ConicConverter.kt
new file mode 100644
index 0000000..445bd47
--- /dev/null
+++ b/graphics/graphics-path/src/main/java/androidx/graphics/path/ConicConverter.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.graphics.path
+
+import android.util.Log
+
+/**
+ * This class converts a given Conic object to the equivalent set of Quadratic objects.
+ * It stores all quadratics from a conversion in the call to [convert], but returns only
+ * one at a time, from nextQuadratic(), storing the rest for later retrieval (since a
+ * PathIterator only retrieves one object at a time).
+ *
+ * This object is stateful, using quadraticCount, currentQuadratic, and quadraticData
+ * to send back the next quadratic when requested, in [nextQuadratic].
+ */
+internal class ConicConverter() {
+
+ private val LOG_TAG = "ConicConverter"
+ private val DEBUG = false
+
+ /**
+ * The total number of quadratics currently stored in the converter
+ */
+ var quadraticCount: Int = 0
+ private set
+
+ /**
+ * The index of the current Quadratic; this is the next quadratic to be returned
+ * in the call to nextQuadratic().
+ */
+ var currentQuadratic = 0
+
+ /**
+ * Storage for all quadratics for a particular conic. Set to reasonable
+ * default size, will need to resize if we ever get a return count larger
+ * than the current size.
+ * Initial size holds up to 5 quadratics: 2 floats/point, 3 points/quadratic
+ * where all quadratics overlap in one point except the ends.
+ */
+ private var quadraticData = FloatArray(1)
+
+ /**
+ * This function stores the next converted quadratic in the given points array,
+ * returning true if this happened, false if there was no quadratic to be returned.
+ */
+ fun nextQuadratic(points: FloatArray, offset: Int = 0): Boolean {
+ if (currentQuadratic < quadraticCount) {
+ val index = currentQuadratic * 2 * 2
+ points[0 + offset] = quadraticData[index]
+ points[1 + offset] = quadraticData[index + 1]
+ points[2 + offset] = quadraticData[index + 2]
+ points[3 + offset] = quadraticData[index + 3]
+ points[4 + offset] = quadraticData[index + 4]
+ points[5 + offset] = quadraticData[index + 5]
+ currentQuadratic++
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Converts the conic in [points] to a series of quadratics, which will all be stored
+ */
+ fun convert(points: FloatArray, weight: Float, tolerance: Float, offset: Int = 0) {
+ quadraticCount = internalConicToQuadratics(points, quadraticData, weight, tolerance, offset)
+ if (quadraticCount > quadraticData.size) {
+ if (DEBUG) Log.d(LOG_TAG, "Resizing quadraticData buffer to $quadraticCount")
+ quadraticData = FloatArray(quadraticCount * 4 * 2)
+ quadraticCount = internalConicToQuadratics(points, quadraticData, weight, tolerance,
+ offset)
+ }
+ currentQuadratic = 0
+ if (DEBUG) Log.d("ConicConverter", "internalConicToQuadratics returned " + quadraticCount)
+ }
+
+ /**
+ * The actual conversion from conic to quadratic data happens in native code, in the library
+ * loaded elsewhere. This JNI function wraps that native functionality.
+ */
+ @Suppress("KotlinJniMissingFunction")
+ private external fun internalConicToQuadratics(
+ conicPoints: FloatArray,
+ quadraticPoints: FloatArray,
+ weight: Float,
+ tolerance: Float,
+ offset: Int
+ ): Int
+}
\ No newline at end of file
diff --git a/graphics/graphics-path/src/main/java/androidx/graphics/path/PathIterator.kt b/graphics/graphics-path/src/main/java/androidx/graphics/path/PathIterator.kt
new file mode 100644
index 0000000..6567419
--- /dev/null
+++ b/graphics/graphics-path/src/main/java/androidx/graphics/path/PathIterator.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:JvmName("PathUtilities")
+package androidx.graphics.path
+
+import android.graphics.Path
+import androidx.core.os.BuildCompat
+import androidx.core.os.BuildCompat.PrereleaseSdkCheck
+
+/**
+ * A path iterator can be used to iterate over all the [segments][PathSegment] that make up
+ * a path. Those segments may in turn define multiple contours inside the path. Conic segments
+ * are by default evaluated as approximated quadratic segments. To preserve conic segments as
+ * conics, set [conicEvaluation] to [AsConic][ConicEvaluation.AsConic]. The error of the
+ * approximation is controlled by [tolerance].
+ *
+ * [PathIterator] objects are created implicitly through a given [Path] object; to create a
+ * [PathIterator], call one of the two [Path.iterator] extension functions.
+ */
+@Suppress("NotCloseable", "IllegalExperimentalApiUsage")
+@PrereleaseSdkCheck
+class PathIterator constructor(
+ val path: Path,
+ val conicEvaluation: ConicEvaluation = ConicEvaluation.AsQuadratics,
+ val tolerance: Float = 0.25f
+) : Iterator<PathSegment> {
+
+ internal val implementation: PathIteratorImpl
+ init {
+ implementation =
+ when {
+ // TODO: replace isAtLeastU() check with below or similar when U is released
+ // Build.VERSION.SDK_INT >= 34 -> {
+ BuildCompat.isAtLeastU() -> {
+ PathIteratorApi34Impl(path, conicEvaluation, tolerance)
+ }
+ else -> {
+ PathIteratorPreApi34Impl(path, conicEvaluation, tolerance)
+ }
+ }
+ }
+
+ enum class ConicEvaluation {
+ /**
+ * Conic segments are returned as conic segments.
+ */
+ AsConic,
+
+ /**
+ * Conic segments are returned as quadratic approximations. The quality of the
+ * approximation is defined by a tolerance value.
+ */
+ AsQuadratics
+ }
+
+ /**
+ * Returns the number of verbs present in this iterator, i.e. the number of calls to
+ * [next] required to complete the iteration.
+ *
+ * By default, [calculateSize] returns the true number of operations in the iterator. Deriving
+ * this result requires converting any conics to quadratics, if [conicEvaluation] is
+ * set to [ConicEvaluation.AsQuadratics], which takes extra processing time. Set
+ * [includeConvertedConics] to false if an approximate size, not including conic
+ * conversion, is sufficient.
+ *
+ * @param includeConvertedConics The returned size includes any required conic conversions.
+ * Default is true, so it will return the exact size, at the cost of iterating through
+ * all elements and converting any conics as appropriate. Set to false to save on processing,
+ * at the cost of a less exact result.
+ */
+ fun calculateSize(includeConvertedConics: Boolean = true) =
+ implementation.calculateSize(includeConvertedConics)
+
+ /**
+ * Returns `true` if the iteration has more elements.
+ */
+ override fun hasNext(): Boolean = implementation.hasNext()
+
+ /**
+ * Returns the type of the current segment in the iteration, or [Done][PathSegment.Type.Done]
+ * if the iteration is finished.
+ */
+ fun peek() = implementation.peek()
+
+ /**
+ * Returns the [type][PathSegment.Type] of the next [path segment][PathSegment] in the iteration
+ * and fills [points] with the points specific to the segment type. Each pair of floats in
+ * the [points] array represents a point for the given segment. The number of pairs of floats
+ * depends on the [PathSegment.Type]:
+ * - [Move][PathSegment.Type.Move]: 1 pair (indices 0 to 1)
+ * - [Move][PathSegment.Type.Line]: 2 pairs (indices 0 to 3)
+ * - [Move][PathSegment.Type.Quadratic]: 3 pairs (indices 0 to 5)
+ * - [Move][PathSegment.Type.Conic]: 4 pairs (indices 0 to 7), the last pair contains the
+ * [weight][PathSegment.weight] twice
+ * - [Move][PathSegment.Type.Cubic]: 4 pairs (indices 0 to 7)
+ * - [Close][PathSegment.Type.Close]: 0 pair
+ * - [Done][PathSegment.Type.Done]: 0 pair
+ * This method does not allocate any memory.
+ *
+ * @param points A [FloatArray] large enough to hold 8 floats starting at [offset],
+ * throws an [IllegalStateException] otherwise.
+ * @param offset Offset in [points] where to store the result
+ */
+ @JvmOverloads
+ fun next(points: FloatArray, offset: Int = 0): PathSegment.Type =
+ implementation.next(points, offset)
+
+ /**
+ * Returns the next [path segment][PathSegment] in the iteration, or [DoneSegment] if
+ * the iteration is finished. To save on allocations, use the alternative [next] function, which
+ * takes a [FloatArray].
+ */
+ override fun next(): PathSegment = implementation.next()
+}
+
+/**
+ * Creates a new [PathIterator] for this [path][android.graphics.Path] that evaluates
+ * conics as quadratics. To preserve conics, use the [Path.iterator] function that takes a
+ * [PathIterator.ConicEvaluation] parameter.
+ */
+@Suppress("IllegalExperimentalApiUsage")
+@PrereleaseSdkCheck
+operator fun Path.iterator() = PathIterator(this)
+
+/**
+ * Creates a new [PathIterator] for this [path][android.graphics.Path]. To preserve conics as
+ * conics (not convert them to quadratics), set [conicEvaluation] to
+ * [PathIterator.ConicEvaluation.AsConic].
+ */
+@Suppress("IllegalExperimentalApiUsage")
+@PrereleaseSdkCheck
+fun Path.iterator(conicEvaluation: PathIterator.ConicEvaluation, tolerance: Float = 0.25f) =
+ PathIterator(this, conicEvaluation, tolerance)
diff --git a/graphics/graphics-path/src/main/java/androidx/graphics/path/PathIteratorImpl.kt b/graphics/graphics-path/src/main/java/androidx/graphics/path/PathIteratorImpl.kt
new file mode 100644
index 0000000..65fa2c1
--- /dev/null
+++ b/graphics/graphics-path/src/main/java/androidx/graphics/path/PathIteratorImpl.kt
@@ -0,0 +1,341 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.graphics.path
+
+import android.graphics.Path
+import android.graphics.PathIterator as PlatformPathIterator
+import android.graphics.PointF
+import androidx.annotation.RequiresApi
+import androidx.core.os.BuildCompat
+import androidx.graphics.path.PathIterator.ConicEvaluation
+
+/**
+ * Base class for API-version-specific PathIterator implementation classes. All functionality
+ * is implemented in the subclasses except for [next], which relies on shared native code
+ * to perform conic conversion.
+ */
+@Suppress("IllegalExperimentalApiUsage")
+@BuildCompat.PrereleaseSdkCheck
+internal abstract class PathIteratorImpl(
+ val path: Path,
+ val conicEvaluation: ConicEvaluation = ConicEvaluation.AsQuadratics,
+ val tolerance: Float = 0.25f
+) {
+ /**
+ * An iterator's ConicConverter converts from a conic to a series of
+ * quadratics. It keeps track of the resulting quadratics and iterates through
+ * them on ensuing calls to next(). The converter is only ever called if
+ * [conicEvaluation] is set to [ConicEvaluation.AsQuadratics].
+ */
+ var conicConverter = ConicConverter()
+
+ /**
+ * pointsData is used internally when the no-arg variant of next() is called,
+ * to avoid allocating a new array every time.
+ */
+ val pointsData = FloatArray(8)
+
+ private companion object {
+ init {
+ /**
+ * The native library is used mainly for pre-API34, but we also rely
+ * on the conic conversion code in API34+, thus it is initialized here.
+ */
+ System.loadLibrary("androidx.graphics.path")
+ }
+ }
+
+ abstract fun calculateSize(includeConvertedConics: Boolean): Int
+
+ abstract fun hasNext(): Boolean
+ abstract fun peek(): PathSegment.Type
+
+ /**
+ * The core functionality of [next] is in API-specific subclasses. But we implement [next]
+ * at this level to share the same conic conversion implementation across all versions.
+ * This happens by calling [nextImpl] to get the next segment from the subclasses, then
+ * calling the shared [ConicConverter] code when appropriate to get and return the
+ * converted segments.
+ */
+ abstract fun nextImpl(points: FloatArray, offset: Int = 0): PathSegment.Type
+
+ fun next(points: FloatArray, offset: Int = 0): PathSegment.Type {
+ check(points.size - offset >= 8) { "The points array must contain at least 8 floats" }
+ // First check to see if we are currently iterating through converted conics
+ if (conicConverter.currentQuadratic < conicConverter.quadraticCount
+ ) {
+ conicConverter.nextQuadratic(points, offset)
+ return (pathSegmentTypes[PathSegment.Type.Quadratic.ordinal])
+ } else {
+ val typeValue = nextImpl(points, offset)
+ if (typeValue == PathSegment.Type.Conic &&
+ conicEvaluation == ConicEvaluation.AsQuadratics
+ ) {
+ with(conicConverter) {
+ convert(points, points[6 + offset], tolerance, offset)
+ if (quadraticCount > 0) {
+ nextQuadratic(points, offset)
+ }
+ }
+ return PathSegment.Type.Quadratic
+ }
+ return typeValue
+ }
+ }
+
+ fun next(): PathSegment {
+ val type = next(pointsData, 0)
+ if (type == PathSegment.Type.Done) return DoneSegment
+ if (type == PathSegment.Type.Close) return CloseSegment
+ val weight = if (type == PathSegment.Type.Conic) pointsData[6] else 0.0f
+ return PathSegment(type, floatsToPoints(pointsData, type), weight)
+ }
+
+ /**
+ * Utility function to convert a FloatArray to an array of PointF objects, where
+ * every two Floats in the FloatArray correspond to a single PointF in the resulting
+ * point array. The FloatArray is used internally to process a next() call, the
+ * array of points is used to create a PathSegment from the operation.
+ */
+ private fun floatsToPoints(pointsData: FloatArray, type: PathSegment.Type): Array<PointF> {
+ val points = when (type) {
+ PathSegment.Type.Move -> {
+ arrayOf(PointF(pointsData[0], pointsData[1]))
+ }
+
+ PathSegment.Type.Line -> {
+ arrayOf(
+ PointF(pointsData[0], pointsData[1]),
+ PointF(pointsData[2], pointsData[3])
+ )
+ }
+
+ PathSegment.Type.Quadratic,
+ PathSegment.Type.Conic -> {
+ arrayOf(
+ PointF(pointsData[0], pointsData[1]),
+ PointF(pointsData[2], pointsData[3]),
+ PointF(pointsData[4], pointsData[5])
+ )
+ }
+
+ PathSegment.Type.Cubic -> {
+ arrayOf(
+ PointF(pointsData[0], pointsData[1]),
+ PointF(pointsData[2], pointsData[3]),
+ PointF(pointsData[4], pointsData[5]),
+ PointF(pointsData[6], pointsData[7])
+ )
+ }
+ // This should not happen because of the early returns above
+ else -> emptyArray()
+ }
+ return points
+ }
+}
+
+/**
+ * In API level 34, we can use new platform functionality for most of what PathIterator does.
+ * The exceptions are conic conversion (which is handled in the base impl class) and
+ * [calculateSize], which is implemented here.
+ */
+@RequiresApi(34)
+@Suppress("IllegalExperimentalApiUsage")
+@BuildCompat.PrereleaseSdkCheck
+internal class PathIteratorApi34Impl(
+ path: Path,
+ conicEvaluation: ConicEvaluation = ConicEvaluation.AsQuadratics,
+ tolerance: Float = 0.25f
+) : PathIteratorImpl(path, conicEvaluation, tolerance) {
+
+ /**
+ * The platform iterator handles most of what we need for iterating. We hold an instance
+ * of that object in this class.
+ */
+ private val platformIterator: PlatformPathIterator
+
+ init {
+ platformIterator = path.pathIterator
+ }
+
+ /**
+ * The platform does not expose a calculateSize() method, so we implement our own. In the
+ * simplest case, this is done by simply iterating through all segments until done. However, if
+ * the caller requested the true size (including any conic conversion) and if there are any
+ * conics in the path segments, then there is more work to do since we have to convert and count
+ * those segments as well.
+ */
+ override fun calculateSize(includeConvertedConics: Boolean): Int {
+ val convertConics = includeConvertedConics &&
+ conicEvaluation == ConicEvaluation.AsQuadratics
+ var numVerbs = 0
+ val tempIterator = path.pathIterator
+ val tempFloats = FloatArray(8)
+ while (tempIterator.hasNext()) {
+ val type = tempIterator.next(tempFloats, 0)
+ if (type == PlatformPathIterator.VERB_CONIC && convertConics) {
+ with(conicConverter) {
+ convert(tempFloats, tempFloats[6], tolerance)
+ numVerbs += quadraticCount
+ }
+ } else {
+ numVerbs++
+ }
+ }
+ return numVerbs
+ }
+
+ /**
+ * [nextImpl] is called by [next] in the base class to do the work of actually getting the
+ * next segment, for which we defer to the platform iterator.
+ */
+ override fun nextImpl(points: FloatArray, offset: Int): PathSegment.Type {
+ return platformToAndroidXSegmentType(platformIterator.next(points, offset))
+ }
+
+ override fun hasNext(): Boolean {
+ return platformIterator.hasNext()
+ }
+
+ override fun peek(): PathSegment.Type {
+ val platformType = platformIterator.peek()
+ return platformToAndroidXSegmentType(platformType)
+ }
+
+ /**
+ * Callers need the AndroidX segment types, so we must convert from the platform types.
+ */
+ private fun platformToAndroidXSegmentType(platformType: Int): PathSegment.Type {
+ return when (platformType) {
+ PlatformPathIterator.VERB_CLOSE -> PathSegment.Type.Close
+ PlatformPathIterator.VERB_CONIC -> PathSegment.Type.Conic
+ PlatformPathIterator.VERB_CUBIC -> PathSegment.Type.Cubic
+ PlatformPathIterator.VERB_DONE -> PathSegment.Type.Done
+ PlatformPathIterator.VERB_LINE -> PathSegment.Type.Line
+ PlatformPathIterator.VERB_MOVE -> PathSegment.Type.Move
+ PlatformPathIterator.VERB_QUAD -> PathSegment.Type.Quadratic
+ else -> {
+ throw IllegalArgumentException("Unknown path segment type $platformType")
+ }
+ }
+ }
+}
+
+/**
+ * Most of the functionality for pre-34 iteration is handled in the native code. The only
+ * exception, similar to the API34 implementation, is the calculateSize(). There is a size()
+ * function in native code which is very quick (it simply tracks the number of verbs in the native
+ * structure). But if the caller wants conic conversion, then we need to iterate through
+ * and convert appropriately, counting as we iterate.
+ */
+@Suppress("IllegalExperimentalApiUsage")
+@BuildCompat.PrereleaseSdkCheck
+internal class PathIteratorPreApi34Impl(
+ path: Path,
+ conicEvaluation: ConicEvaluation = ConicEvaluation.AsQuadratics,
+ tolerance: Float = 0.25f
+) : PathIteratorImpl(path, conicEvaluation, tolerance) {
+
+ @Suppress("KotlinJniMissingFunction")
+ private external fun createInternalPathIterator(
+ path: Path,
+ conicEvaluation: Int,
+ tolerance: Float
+ ): Long
+
+ @Suppress("KotlinJniMissingFunction")
+ private external fun destroyInternalPathIterator(internalPathIterator: Long)
+
+ @Suppress("KotlinJniMissingFunction")
+ private external fun internalPathIteratorHasNext(internalPathIterator: Long): Boolean
+
+ @Suppress("KotlinJniMissingFunction")
+ private external fun internalPathIteratorNext(
+ internalPathIterator: Long,
+ points: FloatArray,
+ offset: Int
+ ): Int
+
+ @Suppress("KotlinJniMissingFunction")
+ private external fun internalPathIteratorPeek(internalPathIterator: Long): Int
+
+ @Suppress("KotlinJniMissingFunction")
+ private external fun internalPathIteratorRawSize(internalPathIterator: Long): Int
+
+ @Suppress("KotlinJniMissingFunction")
+ private external fun internalPathIteratorSize(internalPathIterator: Long): Int
+ /**
+ * Defines the type of evaluation to apply to conic segments during iteration.
+ */
+
+ private val internalPathIterator =
+ createInternalPathIterator(path, ConicEvaluation.AsConic.ordinal, tolerance)
+
+ /**
+ * Returns the number of verbs present in this iterator's path. If [includeConvertedConics]
+ * property is false and the path has any conic elements, the returned size might be smaller
+ * than the number of calls to [next] required to fully iterate over the path. An accurate
+ * size can be computed by setting the parameter to true instead, at a performance cost.
+ * Including converted conics requires iterating through the entire path, including converting
+ * any conics along the way, to calculate the true size.
+ */
+ override fun calculateSize(includeConvertedConics: Boolean): Int {
+ var numVerbs = 0
+ if (!includeConvertedConics || conicEvaluation == ConicEvaluation.AsConic) {
+ numVerbs = internalPathIteratorSize(internalPathIterator)
+ } else {
+ val tempIterator =
+ createInternalPathIterator(path, ConicEvaluation.AsConic.ordinal, tolerance)
+ val tempFloats = FloatArray(8)
+ while (internalPathIteratorHasNext(tempIterator)) {
+ val segment = internalPathIteratorNext(tempIterator, tempFloats, 0)
+ when (pathSegmentTypes[segment]) {
+ PathSegment.Type.Conic -> {
+ conicConverter.convert(tempFloats, tempFloats[7], tolerance)
+ numVerbs += conicConverter.quadraticCount
+ }
+ else -> numVerbs++
+ }
+ }
+ }
+ return numVerbs
+ }
+
+ /**
+ * Returns `true` if the iteration has more elements.
+ */
+ override fun hasNext(): Boolean = internalPathIteratorHasNext(internalPathIterator)
+
+ /**
+ * Returns the type of the current segment in the iteration, or [Done][PathSegment.Type.Done]
+ * if the iteration is finished.
+ */
+ override fun peek() = pathSegmentTypes[internalPathIteratorPeek(internalPathIterator)]
+
+ /**
+ * This is where the actual work happens to get the next segment in the path, which happens
+ * in native code. This function is called by [next] in the base class, which then converts
+ * the resulting segment from conics to quadratics as necessary.
+ */
+ override fun nextImpl(points: FloatArray, offset: Int): PathSegment.Type {
+ return pathSegmentTypes[internalPathIteratorNext(internalPathIterator, points, offset)]
+ }
+
+ protected fun finalize() {
+ destroyInternalPathIterator(internalPathIterator)
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-path/src/main/java/androidx/graphics/path/PathSegment.kt b/graphics/graphics-path/src/main/java/androidx/graphics/path/PathSegment.kt
new file mode 100644
index 0000000..863d6d0
--- /dev/null
+++ b/graphics/graphics-path/src/main/java/androidx/graphics/path/PathSegment.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("PathSegmentUtilities")
+package androidx.graphics.path
+
+import android.graphics.PointF
+
+/**
+ * A path segment represents a curve (line, cubic, quadratic or conic) or a command inside
+ * a fully formed [path][android.graphics.Path] object.
+ *
+ * A segment is identified by a [type][PathSegment.Type] which in turns defines how many
+ * [points] are available (from 0 to 3) and whether the [weight] is meaningful. Please refer
+ * to the documentation of each [type][PathSegment.Type] for more information.
+ *
+ * A segment with the [Move][Type.Move] or [Close][Type.Close] is usually represented by
+ * the singletons [DoneSegment] and [CloseSegment] respectively.
+ *
+ * @property type The type that identifies this segment and defines the number of points.
+ * @property points An array of points describing this segment, whose size depends on [type].
+ * @property weight Conic weight, only valid if [type] is [Type.Conic].
+ */
+class PathSegment internal constructor(
+ val type: Type,
+ @get:Suppress("ArrayReturn") val points: Array<PointF>,
+ val weight: Float
+) {
+
+ /**
+ * Type of a given segment in a [path][android.graphics.Path], either a command
+ * ([Type.Move], [Type.Close], [Type.Done]) or a curve ([Type.Line], [Type.Cubic],
+ * [Type.Quadratic], [Type.Conic]).
+ */
+ enum class Type {
+ /**
+ * Move command, the path segment contains 1 point indicating the move destination.
+ * The weight is set 0.0f and not meaningful.
+ */
+ Move,
+ /**
+ * Line curve, the path segment contains 2 points indicating the two extremities of
+ * the line. The weight is set 0.0f and not meaningful.
+ */
+ Line,
+ /**
+ * Quadratic curve, the path segment contains 3 points in the following order:
+ * - Start point
+ * - Control point
+ * - End point
+ *
+ * The weight is set 0.0f and not meaningful.
+ */
+ Quadratic,
+ /**
+ * Conic curve, the path segment contains 3 points in the following order:
+ * - Start point
+ * - Control point
+ * - End point
+ *
+ * The curve is weighted by the [weight][PathSegment.weight] property.
+ */
+ Conic,
+ /**
+ * Cubic curve, the path segment contains 4 points in the following order:
+ * - Start point
+ * - First control point
+ * - Second control point
+ * - End point
+ *
+ * The weight is set 0.0f and not meaningful.
+ */
+ Cubic,
+ /**
+ * Close command, close the current contour by joining the last point added to the
+ * path with the first point of the current contour. The segment does not contain
+ * any point. The weight is set 0.0f and not meaningful.
+ */
+ Close,
+ /**
+ * Done command, which indicates that no further segment will be
+ * found in the path. It typically indicates the end of an iteration over a path
+ * and can be ignored.
+ */
+ Done
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as PathSegment
+
+ if (type != other.type) return false
+ if (!points.contentEquals(other.points)) return false
+ if (weight != other.weight) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = type.hashCode()
+ result = 31 * result + points.contentHashCode()
+ result = 31 * result + weight.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "PathSegment(type=$type, points=${points.contentToString()}, weight=$weight)"
+ }
+}
+
+/**
+ * A [PathSegment] containing the [Done][PathSegment.Type.Done] command.
+ * This static object exists to avoid allocating a new segment when returning a
+ * [Done][PathSegment.Type.Done] result from [PathIterator.next].
+ */
+val DoneSegment = PathSegment(PathSegment.Type.Done, emptyArray(), 0.0f)
+
+/**
+ * A [PathSegment] containing the [Close][PathSegment.Type.Close] command.
+ * This static object exists to avoid allocating a new segment when returning a
+ * [Close][PathSegment.Type.Close] result from [PathIterator.next].
+ */
+val CloseSegment = PathSegment(PathSegment.Type.Close, emptyArray(), 0.0f)
+
+/**
+ * Cache of [PathSegment.Type] values to avoid internal allocation on each use.
+ */
+internal val pathSegmentTypes = PathSegment.Type.values()
\ No newline at end of file
diff --git a/graphics/graphics-shapes/build.gradle b/graphics/graphics-shapes/build.gradle
index f04e619..5e5dfc1 100644
--- a/graphics/graphics-shapes/build.gradle
+++ b/graphics/graphics-shapes/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "Android Graphics Shapes"
+ name = "Graphics Shapes"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.GRAPHICS_SHAPES
inceptionYear = "2022"
diff --git a/gridlayout/gridlayout/build.gradle b/gridlayout/gridlayout/build.gradle
index 44db995..9fdbcdb 100644
--- a/gridlayout/gridlayout/build.gradle
+++ b/gridlayout/gridlayout/build.gradle
@@ -17,7 +17,7 @@
}
androidx {
- name = "Android Support Grid Layout"
+ name = "Grid Layout"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2013"
description = "Android Support Grid Layout"
diff --git a/health/connect/connect-client-proto/build.gradle b/health/connect/connect-client-proto/build.gradle
index 0458446..0ed136e 100644
--- a/health/connect/connect-client-proto/build.gradle
+++ b/health/connect/connect-client-proto/build.gradle
@@ -78,7 +78,7 @@
artifacts.add(jarjarConf.name, preferencesProtoJarJarTask.flatMap { it.archiveFile })
androidx {
- name = "AndroidX Health Connect Client Proto"
+ name = "Health Connect Client Proto"
publish = Publish.NONE
inceptionYear = "2022"
description = "Proto files for health-connect-client"
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index 0a8378c..7eecedd 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -9,20 +9,12 @@
method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
- method public default static String getHealthConnectSettingsAction();
- method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
- method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
method public androidx.health.connect.client.PermissionController getPermissionController();
- method public default static int getSdkStatus(android.content.Context context, optional String providerPackageName);
- method public default static int getSdkStatus(android.content.Context context);
method public suspend Object? insertRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.InsertRecordsResponse>);
method @Deprecated public default static boolean isApiSupported();
- method @Deprecated public default static boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
- method @Deprecated public default static boolean isProviderAvailable(android.content.Context context);
method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecord(kotlin.reflect.KClass<T> recordType, String recordId, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordResponse<T>>);
method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecords(androidx.health.connect.client.request.ReadRecordsRequest<T> request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordsResponse<T>>);
method public suspend Object? updateRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- property public default static String ACTION_HEALTH_CONNECT_SETTINGS;
property public abstract androidx.health.connect.client.PermissionController permissionController;
field public static final androidx.health.connect.client.HealthConnectClient.Companion Companion;
field public static final int SDK_AVAILABLE = 3; // 0x3
@@ -31,31 +23,19 @@
}
public static final class HealthConnectClient.Companion {
- method public String getHealthConnectSettingsAction();
- method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
- method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
- method public int getSdkStatus(android.content.Context context, optional String providerPackageName);
- method public int getSdkStatus(android.content.Context context);
method @Deprecated public boolean isApiSupported();
- method @Deprecated public boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
- method @Deprecated public boolean isProviderAvailable(android.content.Context context);
- property public final String ACTION_HEALTH_CONNECT_SETTINGS;
field public static final int SDK_AVAILABLE = 3; // 0x3
field public static final int SDK_UNAVAILABLE = 1; // 0x1
field public static final int SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED = 2; // 0x2
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface PermissionController {
- method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract(optional String providerPackageName);
- method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract();
method public suspend Object? getGrantedPermissions(kotlin.coroutines.Continuation<? super java.util.Set<? extends java.lang.String>>);
method public suspend Object? revokeAllPermissions(kotlin.coroutines.Continuation<? super kotlin.Unit>);
field public static final androidx.health.connect.client.PermissionController.Companion Companion;
}
public static final class PermissionController.Companion {
- method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract(optional String providerPackageName);
- method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract();
}
}
diff --git a/health/connect/connect-client/api/public_plus_experimental_current.txt b/health/connect/connect-client/api/public_plus_experimental_current.txt
index 0a8378c..d2856cc 100644
--- a/health/connect/connect-client/api/public_plus_experimental_current.txt
+++ b/health/connect/connect-client/api/public_plus_experimental_current.txt
@@ -9,20 +9,20 @@
method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
- method public default static String getHealthConnectSettingsAction();
- method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
- method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public default static String getHealthConnectSettingsAction();
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
method public androidx.health.connect.client.PermissionController getPermissionController();
- method public default static int getSdkStatus(android.content.Context context, optional String providerPackageName);
- method public default static int getSdkStatus(android.content.Context context);
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public default static int getSdkStatus(android.content.Context context, optional String providerPackageName);
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public default static int getSdkStatus(android.content.Context context);
method public suspend Object? insertRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.InsertRecordsResponse>);
method @Deprecated public default static boolean isApiSupported();
- method @Deprecated public default static boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
- method @Deprecated public default static boolean isProviderAvailable(android.content.Context context);
+ method @Deprecated @androidx.core.os.BuildCompat.PrereleaseSdkCheck public default static boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
+ method @Deprecated @androidx.core.os.BuildCompat.PrereleaseSdkCheck public default static boolean isProviderAvailable(android.content.Context context);
method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecord(kotlin.reflect.KClass<T> recordType, String recordId, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordResponse<T>>);
method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecords(androidx.health.connect.client.request.ReadRecordsRequest<T> request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordsResponse<T>>);
method public suspend Object? updateRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- property public default static String ACTION_HEALTH_CONNECT_SETTINGS;
+ property @androidx.core.os.BuildCompat.PrereleaseSdkCheck public default static String ACTION_HEALTH_CONNECT_SETTINGS;
property public abstract androidx.health.connect.client.PermissionController permissionController;
field public static final androidx.health.connect.client.HealthConnectClient.Companion Companion;
field public static final int SDK_AVAILABLE = 3; // 0x3
@@ -31,31 +31,31 @@
}
public static final class HealthConnectClient.Companion {
- method public String getHealthConnectSettingsAction();
- method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
- method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
- method public int getSdkStatus(android.content.Context context, optional String providerPackageName);
- method public int getSdkStatus(android.content.Context context);
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public String getHealthConnectSettingsAction();
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public int getSdkStatus(android.content.Context context, optional String providerPackageName);
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public int getSdkStatus(android.content.Context context);
method @Deprecated public boolean isApiSupported();
- method @Deprecated public boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
- method @Deprecated public boolean isProviderAvailable(android.content.Context context);
- property public final String ACTION_HEALTH_CONNECT_SETTINGS;
+ method @Deprecated @androidx.core.os.BuildCompat.PrereleaseSdkCheck public boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
+ method @Deprecated @androidx.core.os.BuildCompat.PrereleaseSdkCheck public boolean isProviderAvailable(android.content.Context context);
+ property @androidx.core.os.BuildCompat.PrereleaseSdkCheck public final String ACTION_HEALTH_CONNECT_SETTINGS;
field public static final int SDK_AVAILABLE = 3; // 0x3
field public static final int SDK_UNAVAILABLE = 1; // 0x1
field public static final int SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED = 2; // 0x2
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface PermissionController {
- method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract(optional String providerPackageName);
- method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract();
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract(optional String providerPackageName);
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract();
method public suspend Object? getGrantedPermissions(kotlin.coroutines.Continuation<? super java.util.Set<? extends java.lang.String>>);
method public suspend Object? revokeAllPermissions(kotlin.coroutines.Continuation<? super kotlin.Unit>);
field public static final androidx.health.connect.client.PermissionController.Companion Companion;
}
public static final class PermissionController.Companion {
- method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract(optional String providerPackageName);
- method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract();
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract(optional String providerPackageName);
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract();
}
}
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 970eb61..87a3cef 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -9,20 +9,12 @@
method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
- method public default static String getHealthConnectSettingsAction();
- method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
- method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
method public androidx.health.connect.client.PermissionController getPermissionController();
- method public default static int getSdkStatus(android.content.Context context, optional String providerPackageName);
- method public default static int getSdkStatus(android.content.Context context);
method public suspend Object? insertRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.InsertRecordsResponse>);
method @Deprecated public default static boolean isApiSupported();
- method @Deprecated public default static boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
- method @Deprecated public default static boolean isProviderAvailable(android.content.Context context);
method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecord(kotlin.reflect.KClass<T> recordType, String recordId, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordResponse<T>>);
method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecords(androidx.health.connect.client.request.ReadRecordsRequest<T> request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordsResponse<T>>);
method public suspend Object? updateRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- property public default static String ACTION_HEALTH_CONNECT_SETTINGS;
property public abstract androidx.health.connect.client.PermissionController permissionController;
field public static final androidx.health.connect.client.HealthConnectClient.Companion Companion;
field public static final int SDK_AVAILABLE = 3; // 0x3
@@ -31,31 +23,19 @@
}
public static final class HealthConnectClient.Companion {
- method public String getHealthConnectSettingsAction();
- method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
- method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
- method public int getSdkStatus(android.content.Context context, optional String providerPackageName);
- method public int getSdkStatus(android.content.Context context);
method @Deprecated public boolean isApiSupported();
- method @Deprecated public boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
- method @Deprecated public boolean isProviderAvailable(android.content.Context context);
- property public final String ACTION_HEALTH_CONNECT_SETTINGS;
field public static final int SDK_AVAILABLE = 3; // 0x3
field public static final int SDK_UNAVAILABLE = 1; // 0x1
field public static final int SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED = 2; // 0x2
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface PermissionController {
- method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract(optional String providerPackageName);
- method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract();
method public suspend Object? getGrantedPermissions(kotlin.coroutines.Continuation<? super java.util.Set<? extends java.lang.String>>);
method public suspend Object? revokeAllPermissions(kotlin.coroutines.Continuation<? super kotlin.Unit>);
field public static final androidx.health.connect.client.PermissionController.Companion Companion;
}
public static final class PermissionController.Companion {
- method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract(optional String providerPackageName);
- method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract();
}
}
diff --git a/health/connect/connect-client/build.gradle b/health/connect/connect-client/build.gradle
index 06b657b..16260ad 100644
--- a/health/connect/connect-client/build.gradle
+++ b/health/connect/connect-client/build.gradle
@@ -40,6 +40,7 @@
implementation(libs.guavaAndroid)
implementation(libs.kotlinCoroutinesAndroid)
implementation(libs.kotlinCoroutinesGuava)
+ implementation("androidx.core:core-ktx:1.8.0")
testImplementation(libs.testCore)
testImplementation(libs.testRunner)
@@ -55,6 +56,13 @@
testImplementation(libs.espressoIntents)
testImplementation(libs.kotlinReflect)
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.kotlinCoroutinesTest)
+ androidTestImplementation(libs.kotlinReflect)
+ androidTestImplementation(libs.kotlinTest)
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.truth)
+
samples(project(":health:connect:connect-client-samples"))
}
@@ -73,7 +81,7 @@
}
androidx {
- name = "AndroidX Health Connect Client Library"
+ name = "Health Connect Client"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2022"
description = "read or write user's health and fitness records."
diff --git a/health/connect/connect-client/lint-baseline.xml b/health/connect/connect-client/lint-baseline.xml
index c5e5522..76a4965 100644
--- a/health/connect/connect-client/lint-baseline.xml
+++ b/health/connect/connect-client/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.1.0-alpha07" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-beta03)" variant="all" version="8.1.0-alpha07">
+<issues format="6" by="lint 8.1.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta01)" variant="all" version="8.1.0-beta01">
<issue
id="BanHideAnnotation"
@@ -155,6 +155,123 @@
</issue>
<issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) "android.health.connect.action.HEALTH_HOME_SETTINGS""
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/health/connect/client/HealthConnectClient.kt"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/health/connect/client/HealthConnectClient.kt"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/health/connect/client/HealthConnectClient.kt"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/health/connect/client/HealthConnectClient.kt"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/health/connect/client/HealthConnectClient.kt"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/health/connect/client/HealthConnectClient.kt"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/health/connect/client/HealthConnectClient.kt"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/health/connect/client/HealthConnectClient.kt"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/health/connect/client/HealthConnectClient.kt"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/health/connect/client/PermissionController.kt"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/health/connect/client/PermissionController.kt"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/health/connect/client/PermissionController.kt"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/health/connect/client/PermissionController.kt"/>
+ </issue>
+
+ <issue
id="RequireUnstableAidlAnnotation"
message="Unstable AIDL files must be annotated with `@RequiresOptIn` marker"
errorLine1="parcelable AggregateDataRequest;"
diff --git a/health/connect/connect-client/samples/build.gradle b/health/connect/connect-client/samples/build.gradle
index 2d8bd45..b1b2388 100644
--- a/health/connect/connect-client/samples/build.gradle
+++ b/health/connect/connect-client/samples/build.gradle
@@ -34,7 +34,7 @@
}
androidx {
- name = "AndroidX Health Connect Library Samples"
+ name = "Health Connect Samples"
type = LibraryType.SAMPLES
inceptionYear = "2022"
description = "Contains the sample code for the Androidx Health Connect Library"
diff --git a/health/connect/connect-client/src/androidTest/AndroidManifest.xml b/health/connect/connect-client/src/androidTest/AndroidManifest.xml
index 4d68dc2..34efdec 100644
--- a/health/connect/connect-client/src/androidTest/AndroidManifest.xml
+++ b/health/connect/connect-client/src/androidTest/AndroidManifest.xml
@@ -15,5 +15,96 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- Read permissions for ACTIVITY. -->
+ <uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"/>
+ <uses-permission android:name="android.permission.health.READ_DISTANCE"/>
+ <uses-permission android:name="android.permission.health.READ_ELEVATION_GAINED"/>
+ <uses-permission android:name="android.permission.health.READ_EXERCISE"/>
+ <uses-permission android:name="android.permission.health.READ_FLOORS_CLIMBED"/>
+ <uses-permission android:name="android.permission.health.READ_STEPS"/>
+ <uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED"/>
+ <uses-permission android:name="android.permission.health.READ_VO2_MAX"/>
+ <uses-permission android:name="android.permission.health.READ_WHEELCHAIR_PUSHES"/>
+ <uses-permission android:name="android.permission.health.READ_POWER"/>
+ <uses-permission android:name="android.permission.health.READ_SPEED"/>
+ <!-- Read permissions for BODY_MEASUREMENTS. -->
+ <uses-permission android:name="android.permission.health.READ_BASAL_METABOLIC_RATE"/>
+ <uses-permission android:name="android.permission.health.READ_BODY_FAT"/>
+ <uses-permission android:name="android.permission.health.READ_BODY_WATER_MASS"/>
+ <uses-permission android:name="android.permission.health.READ_BONE_MASS"/>
+ <uses-permission android:name="android.permission.health.READ_HEIGHT"/>
+ <uses-permission android:name="android.permission.health.READ_LEAN_BODY_MASS"/>
+ <uses-permission android:name="android.permission.health.READ_WEIGHT"/>
+
+ <!-- Read permissions for CYCLE_TRACKING. -->
+ <uses-permission android:name="android.permission.health.READ_CERVICAL_MUCUS"/>
+ <uses-permission android:name="android.permission.health.READ_MENSTRUATION"/>
+ <uses-permission android:name="android.permission.health.READ_OVULATION_TEST"/>
+ <uses-permission android:name="android.permission.health.READ_SEXUAL_ACTIVITY"/>
+
+ <!-- Read permissions for NUTRITION. -->
+ <uses-permission android:name="android.permission.health.READ_HYDRATION"/>
+ <uses-permission android:name="android.permission.health.READ_NUTRITION"/>
+
+ <!-- Read permissions for SLEEP. -->
+ <uses-permission android:name="android.permission.health.READ_SLEEP"/>
+
+ <!-- Read permissions for VITALS. -->
+ <uses-permission android:name="android.permission.health.READ_BASAL_BODY_TEMPERATURE"/>
+ <uses-permission android:name="android.permission.health.READ_BLOOD_GLUCOSE"/>
+ <uses-permission android:name="android.permission.health.READ_BLOOD_PRESSURE"/>
+ <uses-permission android:name="android.permission.health.READ_BODY_TEMPERATURE"/>
+ <uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
+ <uses-permission android:name="android.permission.health.READ_HEART_RATE_VARIABILITY"/>
+ <uses-permission android:name="android.permission.health.READ_OXYGEN_SATURATION"/>
+ <uses-permission android:name="android.permission.health.READ_RESPIRATORY_RATE"/>
+ <uses-permission android:name="android.permission.health.READ_RESTING_HEART_RATE"/>
+
+ <!-- Write permissions for ACTIVITY. -->
+ <uses-permission android:name="android.permission.health.WRITE_ACTIVE_CALORIES_BURNED"/>
+ <uses-permission android:name="android.permission.health.WRITE_DISTANCE"/>
+ <uses-permission android:name="android.permission.health.WRITE_ELEVATION_GAINED"/>
+ <uses-permission android:name="android.permission.health.WRITE_EXERCISE"/>
+ <uses-permission android:name="android.permission.health.WRITE_FLOORS_CLIMBED"/>
+ <uses-permission android:name="android.permission.health.WRITE_STEPS"/>
+ <uses-permission android:name="android.permission.health.WRITE_TOTAL_CALORIES_BURNED"/>
+ <uses-permission android:name="android.permission.health.WRITE_VO2_MAX"/>
+ <uses-permission android:name="android.permission.health.WRITE_WHEELCHAIR_PUSHES"/>
+ <uses-permission android:name="android.permission.health.WRITE_POWER"/>
+ <uses-permission android:name="android.permission.health.WRITE_SPEED"/>
+
+ <!-- Write permissions for BODY_MEASUREMENTS. -->
+ <uses-permission android:name="android.permission.health.WRITE_BASAL_METABOLIC_RATE"/>
+ <uses-permission android:name="android.permission.health.WRITE_BODY_FAT"/>
+ <uses-permission android:name="android.permission.health.WRITE_BODY_WATER_MASS"/>
+ <uses-permission android:name="android.permission.health.WRITE_BONE_MASS"/>
+ <uses-permission android:name="android.permission.health.WRITE_HEIGHT"/>
+ <uses-permission android:name="android.permission.health.WRITE_LEAN_BODY_MASS"/>
+ <uses-permission android:name="android.permission.health.WRITE_WEIGHT"/>
+
+ <!-- Write permissions for CYCLE_TRACKING. -->
+ <uses-permission android:name="android.permission.health.WRITE_CERVICAL_MUCUS"/>
+ <uses-permission android:name="android.permission.health.WRITE_INTERMENSTRUAL_BLEEDING"/>
+ <uses-permission android:name="android.permission.health.WRITE_MENSTRUATION"/>
+ <uses-permission android:name="android.permission.health.WRITE_OVULATION_TEST"/>
+ <uses-permission android:name="android.permission.health.WRITE_SEXUAL_ACTIVITY"/>
+
+ <!-- Write permissions for NUTRITION. -->
+ <uses-permission android:name="android.permission.health.WRITE_HYDRATION"/>
+ <uses-permission android:name="android.permission.health.WRITE_NUTRITION"/>
+
+ <!-- Write permissions for SLEEP. -->
+ <uses-permission android:name="android.permission.health.WRITE_SLEEP"/>
+
+ <!-- Write permissions for VITALS. -->
+ <uses-permission android:name="android.permission.health.WRITE_BASAL_BODY_TEMPERATURE"/>
+ <uses-permission android:name="android.permission.health.WRITE_BLOOD_GLUCOSE"/>
+ <uses-permission android:name="android.permission.health.WRITE_BLOOD_PRESSURE"/>
+ <uses-permission android:name="android.permission.health.WRITE_BODY_TEMPERATURE"/>
+ <uses-permission android:name="android.permission.health.WRITE_HEART_RATE"/>
+ <uses-permission android:name="android.permission.health.WRITE_HEART_RATE_VARIABILITY"/>
+ <uses-permission android:name="android.permission.health.WRITE_OXYGEN_SATURATION"/>
+ <uses-permission android:name="android.permission.health.WRITE_RESPIRATORY_RATE"/>
+ <uses-permission android:name="android.permission.health.WRITE_RESTING_HEART_RATE"/>
</manifest>
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/ClassFinder.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/ClassFinder.kt
new file mode 100644
index 0000000..5539e49
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/ClassFinder.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.health.connect.client
+
+import androidx.health.connect.client.records.Record
+import java.io.File
+import java.net.URL
+import java.util.zip.ZipEntry
+import java.util.zip.ZipInputStream
+import kotlin.reflect.KClass
+
+@Suppress("UNCHECKED_CAST")
+val RECORD_CLASSES: List<KClass<out Record>> by lazy {
+ findClasses("androidx.health.connect.client.records")
+ .filterNot { it.java.isInterface }
+ .filter { it.simpleName.orEmpty().endsWith("Record") }
+ .map { it as KClass<out Record> }
+}
+
+fun findClasses(packageName: String): Set<KClass<*>> {
+ val resources =
+ requireNotNull(Thread.currentThread().contextClassLoader)
+ .getResources(packageName.replace('.', '/'))
+
+ return buildSet {
+ while (resources.hasMoreElements()) {
+ val classNames = findClasses(resources.nextElement().file, packageName)
+ for (className in classNames) {
+ add(Class.forName(className).kotlin)
+ }
+ }
+ }
+}
+
+private fun findClasses(directory: String, packageName: String): Set<String> = buildSet {
+ if (directory.startsWith("file:") && ('!' in directory)) {
+ addAll(unzipClasses(path = directory, packageName = packageName))
+ }
+
+ for (file in File(directory).takeIf(File::exists)?.listFiles() ?: emptyArray()) {
+ if (file.isDirectory) {
+ addAll(findClasses(file.absolutePath, "$packageName.${file.name}"))
+ } else if (file.name.endsWith(".class")) {
+ add("$packageName.${file.name.dropLast(6)}")
+ }
+ }
+}
+
+private fun unzipClasses(path: String, packageName: String): Set<String> =
+ ZipInputStream(URL(path.substringBefore('!')).openStream()).use { zip ->
+ buildSet {
+ while (true) {
+ val entry = zip.nextEntry ?: break
+ val className = entry.formatClassName()
+ if ((className != null) && className.startsWith(packageName)) {
+ add(className)
+ }
+ }
+ }
+ }
+
+private fun ZipEntry.formatClassName(): String? =
+ name
+ .takeIf { it.endsWith(".class") }
+ ?.replace("[$].*".toRegex(), "")
+ ?.replace("[.]class".toRegex(), "")
+ ?.replace('/', '.')
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
new file mode 100644
index 0000000..7895078
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
@@ -0,0 +1,537 @@
+/*
+ * 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.health.connect.client.impl
+
+import android.annotation.TargetApi
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.RemoteException
+import androidx.health.connect.client.HealthConnectClient
+import androidx.health.connect.client.changes.DeletionChange
+import androidx.health.connect.client.changes.UpsertionChange
+import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_PREFIX
+import androidx.health.connect.client.records.HeartRateRecord
+import androidx.health.connect.client.records.NutritionRecord
+import androidx.health.connect.client.records.StepsRecord
+import androidx.health.connect.client.records.WheelchairPushesRecord
+import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.request.AggregateGroupByDurationRequest
+import androidx.health.connect.client.request.AggregateGroupByPeriodRequest
+import androidx.health.connect.client.request.AggregateRequest
+import androidx.health.connect.client.request.ChangesTokenRequest
+import androidx.health.connect.client.request.ReadRecordsRequest
+import androidx.health.connect.client.time.TimeRangeFilter
+import androidx.health.connect.client.units.Energy
+import androidx.health.connect.client.units.Mass
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.rule.GrantPermissionRule
+import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.Period
+import java.time.ZoneOffset
+import kotlin.test.assertFailsWith
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+@MediumTest
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+// Comment the SDK suppress to run on emulators lower than U.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+class HealthConnectClientUpsideDownImplTest {
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+ private val allHealthPermissions =
+ context.packageManager
+ .getPackageInfo(
+ context.packageName,
+ PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong())
+ )
+ .requestedPermissions
+ .filter { it.startsWith(PERMISSION_PREFIX) }
+ .toTypedArray()
+
+ // Grant every permission as deletion by id checks for every permission
+ @get:Rule
+ val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(*allHealthPermissions)
+
+ private lateinit var healthConnectClient: HealthConnectClient
+
+ @Before
+ fun setUp() {
+ healthConnectClient = HealthConnectClientUpsideDownImpl(context)
+ }
+
+ @After
+ fun tearDown() = runTest {
+ healthConnectClient.deleteRecords(StepsRecord::class, TimeRangeFilter.none())
+ healthConnectClient.deleteRecords(HeartRateRecord::class, TimeRangeFilter.none())
+ healthConnectClient.deleteRecords(NutritionRecord::class, TimeRangeFilter.none())
+ }
+
+ @Test
+ fun insertRecords() = runTest {
+ val response =
+ healthConnectClient.insertRecords(
+ listOf(
+ StepsRecord(
+ count = 10,
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = null,
+ endTime = Instant.ofEpochMilli(5678L),
+ endZoneOffset = null
+ )
+ )
+ )
+ assertThat(response.recordIdsList).hasSize(1)
+ }
+
+ @Test
+ @Ignore("b/270954533")
+ fun deleteRecords_byId() = runTest {
+ val recordIds =
+ healthConnectClient
+ .insertRecords(
+ listOf(
+ StepsRecord(
+ count = 10,
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = null,
+ endTime = Instant.ofEpochMilli(5678L),
+ endZoneOffset = null
+ ),
+ StepsRecord(
+ count = 15,
+ startTime = Instant.ofEpochMilli(12340L),
+ startZoneOffset = null,
+ endTime = Instant.ofEpochMilli(56780L),
+ endZoneOffset = null
+ ),
+ StepsRecord(
+ count = 20,
+ startTime = Instant.ofEpochMilli(123400L),
+ startZoneOffset = null,
+ endTime = Instant.ofEpochMilli(567800L),
+ endZoneOffset = null,
+ metadata = Metadata(clientRecordId = "clientId")
+ ),
+ )
+ )
+ .recordIdsList
+
+ val initialRecords =
+ healthConnectClient
+ .readRecords(ReadRecordsRequest(StepsRecord::class, TimeRangeFilter.none()))
+ .records
+
+ healthConnectClient.deleteRecords(
+ StepsRecord::class,
+ listOf(recordIds[1]),
+ listOf("clientId")
+ )
+
+ assertThat(
+ healthConnectClient
+ .readRecords(ReadRecordsRequest(StepsRecord::class, TimeRangeFilter.none()))
+ .records
+ )
+ .containsExactly(initialRecords[0])
+ }
+
+ // TODO(b/264253708): remove @Ignore from this test case once bug is resolved
+ @Test
+ @Ignore("Blocked while investigating b/264253708")
+ fun deleteRecords_byTimeRange() = runTest {
+ healthConnectClient
+ .insertRecords(
+ listOf(
+ StepsRecord(
+ count = 100,
+ startTime = Instant.ofEpochMilli(1_234L),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = Instant.ofEpochMilli(5_678L),
+ endZoneOffset = ZoneOffset.UTC
+ ),
+ StepsRecord(
+ count = 150,
+ startTime = Instant.ofEpochMilli(12_340L),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = Instant.ofEpochMilli(56_780L),
+ endZoneOffset = ZoneOffset.UTC
+ ),
+ )
+ )
+ .recordIdsList
+
+ val initialRecords =
+ healthConnectClient
+ .readRecords(ReadRecordsRequest(StepsRecord::class, TimeRangeFilter.none()))
+ .records
+
+ healthConnectClient.deleteRecords(
+ StepsRecord::class,
+ TimeRangeFilter.before(Instant.ofEpochMilli(10_000L))
+ )
+
+ assertThat(
+ healthConnectClient
+ .readRecords(ReadRecordsRequest(StepsRecord::class, TimeRangeFilter.none()))
+ .records
+ )
+ .containsExactly(initialRecords[1])
+ }
+
+ @Test
+ @Ignore("b/270954533")
+ fun updateRecords() = runTest {
+ val id =
+ healthConnectClient
+ .insertRecords(
+ listOf(
+ StepsRecord(
+ count = 10,
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = null,
+ endTime = Instant.ofEpochMilli(5678L),
+ endZoneOffset = null
+ )
+ )
+ )
+ .recordIdsList[0]
+
+ val insertedRecord = healthConnectClient.readRecord(StepsRecord::class, id).record
+
+ healthConnectClient.updateRecords(
+ listOf(
+ StepsRecord(
+ count = 5,
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = null,
+ endTime = Instant.ofEpochMilli(5678L),
+ endZoneOffset = null,
+ metadata = Metadata(id, insertedRecord.metadata.dataOrigin)
+ )
+ )
+ )
+
+ val updatedRecord = healthConnectClient.readRecord(StepsRecord::class, id).record
+
+ assertThat(updatedRecord.count).isEqualTo(5L)
+ }
+
+ @Test
+ @Ignore("b/270954533")
+ fun readRecord_withId() = runTest {
+ val insertResponse =
+ healthConnectClient.insertRecords(
+ listOf(
+ StepsRecord(
+ count = 10,
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = Instant.ofEpochMilli(5678L),
+ endZoneOffset = ZoneOffset.UTC
+ )
+ )
+ )
+
+ val readResponse =
+ healthConnectClient.readRecord(StepsRecord::class, insertResponse.recordIdsList[0])
+
+ with(readResponse.record) {
+ assertThat(count).isEqualTo(10)
+ assertThat(startTime).isEqualTo(Instant.ofEpochMilli(1234L))
+ assertThat(startZoneOffset).isEqualTo(ZoneOffset.UTC)
+ assertThat(endTime).isEqualTo(Instant.ofEpochMilli(5678L))
+ assertThat(endZoneOffset).isEqualTo(ZoneOffset.UTC)
+ }
+ }
+
+ @Test
+ @Ignore("b/270954533")
+ fun readRecords_withFilters() = runTest {
+ healthConnectClient.insertRecords(
+ listOf(
+ StepsRecord(
+ count = 10,
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = Instant.ofEpochMilli(5678L),
+ endZoneOffset = ZoneOffset.UTC
+ ),
+ StepsRecord(
+ count = 5,
+ startTime = Instant.ofEpochMilli(12340L),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = Instant.ofEpochMilli(56780L),
+ endZoneOffset = ZoneOffset.UTC
+ ),
+ )
+ )
+
+ val readResponse =
+ healthConnectClient.readRecords(
+ ReadRecordsRequest(
+ StepsRecord::class,
+ TimeRangeFilter.after(Instant.ofEpochMilli(10_000L))
+ )
+ )
+
+ assertThat(readResponse.records[0].count).isEqualTo(5)
+ }
+
+ @Test
+ @Ignore("b/270954533")
+ fun readRecord_noRecords_throwRemoteException() = runTest {
+ assertFailsWith<RemoteException> { healthConnectClient.readRecord(StepsRecord::class, "1") }
+ }
+
+ @Test
+ @Ignore("b/270954533")
+ fun aggregateRecords() = runTest {
+ healthConnectClient.insertRecords(
+ listOf(
+ StepsRecord(
+ count = 10,
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = Instant.ofEpochMilli(5678L),
+ endZoneOffset = ZoneOffset.UTC
+ ),
+ StepsRecord(
+ count = 5,
+ startTime = Instant.ofEpochMilli(12340L),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = Instant.ofEpochMilli(56780L),
+ endZoneOffset = ZoneOffset.UTC
+ ),
+ HeartRateRecord(
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = Instant.ofEpochMilli(5678L),
+ endZoneOffset = ZoneOffset.UTC,
+ samples =
+ listOf(
+ HeartRateRecord.Sample(Instant.ofEpochMilli(1234L), 57L),
+ HeartRateRecord.Sample(Instant.ofEpochMilli(1235L), 120L)
+ )
+ ),
+ HeartRateRecord(
+ startTime = Instant.ofEpochMilli(12340L),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = Instant.ofEpochMilli(56780L),
+ endZoneOffset = ZoneOffset.UTC,
+ samples =
+ listOf(
+ HeartRateRecord.Sample(Instant.ofEpochMilli(12340L), 47L),
+ HeartRateRecord.Sample(Instant.ofEpochMilli(12350L), 48L)
+ )
+ ),
+ NutritionRecord(
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = Instant.ofEpochMilli(5678L),
+ endZoneOffset = ZoneOffset.UTC,
+ energy = Energy.kilocalories(200.0)
+ )
+ )
+ )
+
+ val aggregateResponse =
+ healthConnectClient.aggregate(
+ AggregateRequest(
+ setOf(
+ StepsRecord.COUNT_TOTAL,
+ HeartRateRecord.BPM_MIN,
+ HeartRateRecord.BPM_MAX,
+ NutritionRecord.ENERGY_TOTAL,
+ NutritionRecord.CAFFEINE_TOTAL,
+ WheelchairPushesRecord.COUNT_TOTAL,
+ ),
+ TimeRangeFilter.none()
+ )
+ )
+
+ with(aggregateResponse) {
+ assertThat(this[StepsRecord.COUNT_TOTAL]).isEqualTo(15L)
+ assertThat(this[HeartRateRecord.BPM_MIN]).isEqualTo(47L)
+ assertThat(this[HeartRateRecord.BPM_MAX]).isEqualTo(120L)
+ assertThat(this[NutritionRecord.ENERGY_TOTAL]).isEqualTo(Energy.kilocalories(200.0))
+ assertThat(this[NutritionRecord.CAFFEINE_TOTAL]).isEqualTo(Mass.grams(0.0))
+
+ assertThat(contains(WheelchairPushesRecord.COUNT_TOTAL)).isFalse()
+ }
+ }
+
+ @Test
+ @Ignore("b/270954533")
+ fun aggregateRecordsGroupByDuration() = runTest {
+ healthConnectClient.insertRecords(
+ listOf(
+ StepsRecord(
+ count = 1,
+ startTime = Instant.ofEpochMilli(1200L),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = Instant.ofEpochMilli(1240L),
+ endZoneOffset = ZoneOffset.UTC
+ ),
+ StepsRecord(
+ count = 2,
+ startTime = Instant.ofEpochMilli(1300L),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = Instant.ofEpochMilli(1500L),
+ endZoneOffset = ZoneOffset.UTC
+ ),
+ StepsRecord(
+ count = 5,
+ startTime = Instant.ofEpochMilli(2400L),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = Instant.ofEpochMilli(3500L),
+ endZoneOffset = ZoneOffset.UTC
+ )
+ )
+ )
+
+ val aggregateResponse =
+ healthConnectClient.aggregateGroupByDuration(
+ AggregateGroupByDurationRequest(
+ setOf(StepsRecord.COUNT_TOTAL),
+ TimeRangeFilter.between(
+ Instant.ofEpochMilli(1000L),
+ Instant.ofEpochMilli(3000L)
+ ),
+ Duration.ofMillis(1000),
+ setOf()
+ )
+ )
+
+ with(aggregateResponse) {
+ assertThat(this).hasSize(2)
+ assertThat(this[0].result[StepsRecord.COUNT_TOTAL]).isEqualTo(3)
+ assertThat(this[1].result[StepsRecord.COUNT_TOTAL]).isEqualTo(5)
+ }
+ }
+
+ @Test
+ @Ignore("Blocked as period response from platform has a bug with inverted start/end timestamps")
+ fun aggregateRecordsGroupByPeriod() = runTest {
+ healthConnectClient.insertRecords(
+ listOf(
+ StepsRecord(
+ count = 100,
+ startTime = LocalDateTime.of(2018, 10, 11, 7, 10).toInstant(ZoneOffset.UTC),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = LocalDateTime.of(2018, 10, 11, 7, 15).toInstant(ZoneOffset.UTC),
+ endZoneOffset = ZoneOffset.UTC
+ ),
+ StepsRecord(
+ count = 200,
+ startTime = LocalDateTime.of(2018, 10, 11, 10, 10).toInstant(ZoneOffset.UTC),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = LocalDateTime.of(2018, 10, 11, 11, 0).toInstant(ZoneOffset.UTC),
+ endZoneOffset = ZoneOffset.UTC
+ ),
+ StepsRecord(
+ count = 50,
+ startTime = LocalDateTime.of(2018, 10, 13, 7, 10).toInstant(ZoneOffset.UTC),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = LocalDateTime.of(2018, 10, 13, 8, 10).toInstant(ZoneOffset.UTC),
+ endZoneOffset = ZoneOffset.UTC
+ )
+ )
+ )
+
+ val aggregateResponse =
+ healthConnectClient.aggregateGroupByPeriod(
+ AggregateGroupByPeriodRequest(
+ setOf(StepsRecord.COUNT_TOTAL),
+ TimeRangeFilter.between(
+ LocalDateTime.of(2018, 10, 11, 6, 10).toInstant(ZoneOffset.UTC),
+ LocalDateTime.of(2018, 10, 12, 7, 15).toInstant(ZoneOffset.UTC),
+ ),
+ timeRangeSlicer = Period.ofDays(1)
+ )
+ )
+
+ with(aggregateResponse) {
+ assertThat(this).hasSize(2)
+ assertThat(this[0].result[StepsRecord.COUNT_TOTAL]).isEqualTo(300)
+ assertThat(this[1].result[StepsRecord.COUNT_TOTAL]).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun getChangesToken() = runTest {
+ val token =
+ healthConnectClient.getChangesToken(
+ ChangesTokenRequest(setOf(StepsRecord::class), setOf())
+ )
+ assertThat(token).isNotEmpty()
+ }
+
+ @Test
+ @Ignore("b/270954533")
+ fun getChanges() = runTest {
+ val token =
+ healthConnectClient.getChangesToken(
+ ChangesTokenRequest(setOf(StepsRecord::class), setOf())
+ )
+
+ val insertedRecordId =
+ healthConnectClient
+ .insertRecords(
+ listOf(
+ StepsRecord(
+ count = 10,
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = ZoneOffset.UTC,
+ endTime = Instant.ofEpochMilli(5678L),
+ endZoneOffset = ZoneOffset.UTC
+ )
+ )
+ )
+ .recordIdsList[0]
+
+ val record = healthConnectClient.readRecord(StepsRecord::class, insertedRecordId).record
+
+ assertThat(healthConnectClient.getChanges(token).changes)
+ .containsExactly(UpsertionChange(record))
+
+ healthConnectClient.deleteRecords(StepsRecord::class, TimeRangeFilter.none())
+
+ assertThat(healthConnectClient.getChanges(token).changes)
+ .containsExactly(DeletionChange(insertedRecordId))
+ }
+
+ @Test
+ fun getGrantedPermissions() = runTest {
+ assertThat(healthConnectClient.permissionController.getGrantedPermissions())
+ .containsExactlyElementsIn(allHealthPermissions)
+ }
+}
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/PermissionControllerUpsideDownTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/PermissionControllerUpsideDownTest.kt
new file mode 100644
index 0000000..3fd857b
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/PermissionControllerUpsideDownTest.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.health.connect.client.impl
+
+import android.annotation.TargetApi
+import android.health.connect.HealthPermissions
+import android.os.Build
+import androidx.health.connect.client.PermissionController
+import androidx.health.connect.client.impl.platform.time.SystemDefaultTimeSource
+import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_PREFIX
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.rule.GrantPermissionRule
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+@MediumTest
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+// Comment the SDK suppress to run on emulators lower than U.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+class PermissionControllerUpsideDownTest {
+
+ @get:Rule
+ val grantPermissionRule: GrantPermissionRule =
+ GrantPermissionRule.grant(HealthPermissions.WRITE_STEPS, HealthPermissions.READ_DISTANCE)
+
+ @Test
+ fun getGrantedPermissions() = runTest {
+ val permissionController: PermissionController =
+ HealthConnectClientUpsideDownImpl(ApplicationProvider.getApplicationContext())
+ // Permissions may have been granted by the other instrumented test in this directory.
+ // Since there is no way to revoke permissions with grantPermissionRule, use containsAtLeast
+ // instead of containsExactly.
+ assertThat(permissionController.getGrantedPermissions())
+ .containsAtLeast(HealthPermissions.WRITE_STEPS, HealthPermissions.READ_DISTANCE)
+ }
+
+ @Test
+ fun revokeAllPermissions_revokesHealthPermissions() = runTest {
+ val revokedPermissions: MutableList<String> = mutableListOf()
+ val permissionController: PermissionController =
+ HealthConnectClientUpsideDownImpl(
+ ApplicationProvider.getApplicationContext(), SystemDefaultTimeSource) {
+ permissionsToRevoke ->
+ revokedPermissions.addAll(permissionsToRevoke)
+ }
+ permissionController.revokeAllPermissions()
+ assertThat(revokedPermissions.all { it.startsWith(PERMISSION_PREFIX) }).isTrue()
+ }
+}
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/MetadataConvertersTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/MetadataConvertersTest.kt
new file mode 100644
index 0000000..3774f34
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/MetadataConvertersTest.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.health.connect.client.impl.platform.records
+
+import android.annotation.TargetApi
+import android.os.Build
+import androidx.health.connect.client.records.metadata.DataOrigin
+import androidx.health.connect.client.records.metadata.Device
+import androidx.health.connect.client.records.metadata.Metadata
+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 java.time.Instant
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+@SmallTest
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+// Comment the SDK suppress to run on emulators lower than U.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+class MetadataConvertersTest {
+
+ fun metadata_convertToPlatform() {
+ val metadata =
+ Metadata(
+ id = "someId",
+ dataOrigin = DataOrigin("origin package name"),
+ lastModifiedTime = Instant.ofEpochMilli(6666L),
+ clientRecordId = "clientId",
+ clientRecordVersion = 2L,
+ device =
+ Device(
+ manufacturer = "Awesome-watches",
+ model = "AwesomeOne",
+ type = Device.TYPE_WATCH))
+
+ with(metadata.toPlatformMetadata()) {
+ assertThat(id).isEqualTo("someId")
+ assertThat(dataOrigin)
+ .isEqualTo(
+ PlatformDataOriginBuilder().setPackageName("origin package name").build())
+ assertThat(clientRecordId).isEqualTo("clientId")
+ assertThat(clientRecordVersion).isEqualTo(2L)
+ assertThat(device)
+ .isEqualTo(
+ PlatformDeviceBuilder()
+ .setManufacturer("Awesome-watches")
+ .setModel("AwesomeOne")
+ .setType(PlatformDevice.DEVICE_TYPE_WATCH)
+ .build())
+ }
+ }
+
+ @Test
+ fun metadata_convertToPlatform_noDevice() {
+ val metadata =
+ Metadata(
+ id = "someId",
+ dataOrigin = DataOrigin("origin package name"),
+ lastModifiedTime = Instant.ofEpochMilli(6666L),
+ clientRecordId = "clientId",
+ clientRecordVersion = 2L)
+
+ with(metadata.toPlatformMetadata()) {
+ assertThat(id).isEqualTo("someId")
+ assertThat(dataOrigin)
+ .isEqualTo(
+ PlatformDataOriginBuilder().setPackageName("origin package name").build())
+ assertThat(clientRecordId).isEqualTo("clientId")
+ assertThat(clientRecordVersion).isEqualTo(2L)
+ assertThat(device).isEqualTo(PlatformDeviceBuilder().build())
+ }
+ }
+
+ @Test
+ fun metadata_convertToSdk() {
+ val metadata =
+ PlatformMetadataBuilder()
+ .apply {
+ setId("someId")
+ setDataOrigin(
+ PlatformDataOriginBuilder().setPackageName("origin package name").build())
+ setLastModifiedTime(Instant.ofEpochMilli(6666L))
+ setClientRecordId("clientId")
+ setClientRecordVersion(2L)
+ setDevice(
+ PlatformDeviceBuilder()
+ .setManufacturer("AwesomeTech")
+ .setModel("AwesomeTwo")
+ .setType(PlatformDevice.DEVICE_TYPE_WATCH)
+ .build())
+ }
+ .build()
+
+ with(metadata.toSdkMetadata()) {
+ assertThat(id).isEqualTo("someId")
+ assertThat(dataOrigin).isEqualTo(DataOrigin("origin package name"))
+ assertThat(lastModifiedTime).isEqualTo(Instant.ofEpochMilli(6666L))
+ assertThat(clientRecordId).isEqualTo("clientId")
+ assertThat(clientRecordVersion).isEqualTo(2L)
+ assertThat(device)
+ .isEqualTo(
+ Device(
+ manufacturer = "AwesomeTech",
+ model = "AwesomeTwo",
+ type = Device.TYPE_WATCH))
+ }
+ }
+}
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RecordConvertersTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RecordConvertersTest.kt
new file mode 100644
index 0000000..a1b2d2a
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RecordConvertersTest.kt
@@ -0,0 +1,1541 @@
+/*
+ * 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.health.connect.client.impl.platform.records
+
+import android.annotation.TargetApi
+import android.os.Build
+import androidx.health.connect.client.RECORD_CLASSES
+import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
+import androidx.health.connect.client.records.BasalBodyTemperatureRecord
+import androidx.health.connect.client.records.BasalMetabolicRateRecord
+import androidx.health.connect.client.records.BloodGlucoseRecord
+import androidx.health.connect.client.records.BloodPressureRecord
+import androidx.health.connect.client.records.BodyFatRecord
+import androidx.health.connect.client.records.BodyTemperatureMeasurementLocation
+import androidx.health.connect.client.records.BodyTemperatureRecord
+import androidx.health.connect.client.records.BodyWaterMassRecord
+import androidx.health.connect.client.records.BoneMassRecord
+import androidx.health.connect.client.records.CervicalMucusRecord
+import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
+import androidx.health.connect.client.records.DistanceRecord
+import androidx.health.connect.client.records.ElevationGainedRecord
+import androidx.health.connect.client.records.ExerciseSessionRecord
+import androidx.health.connect.client.records.FloorsClimbedRecord
+import androidx.health.connect.client.records.HeartRateRecord
+import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
+import androidx.health.connect.client.records.HeightRecord
+import androidx.health.connect.client.records.HydrationRecord
+import androidx.health.connect.client.records.InstantaneousRecord
+import androidx.health.connect.client.records.IntermenstrualBleedingRecord
+import androidx.health.connect.client.records.IntervalRecord
+import androidx.health.connect.client.records.LeanBodyMassRecord
+import androidx.health.connect.client.records.MealType
+import androidx.health.connect.client.records.MenstruationFlowRecord
+import androidx.health.connect.client.records.MenstruationPeriodRecord
+import androidx.health.connect.client.records.NutritionRecord
+import androidx.health.connect.client.records.OvulationTestRecord
+import androidx.health.connect.client.records.OxygenSaturationRecord
+import androidx.health.connect.client.records.PowerRecord
+import androidx.health.connect.client.records.RespiratoryRateRecord
+import androidx.health.connect.client.records.RestingHeartRateRecord
+import androidx.health.connect.client.records.SexualActivityRecord
+import androidx.health.connect.client.records.SleepSessionRecord
+import androidx.health.connect.client.records.SpeedRecord
+import androidx.health.connect.client.records.StepsCadenceRecord
+import androidx.health.connect.client.records.StepsRecord
+import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
+import androidx.health.connect.client.records.Vo2MaxRecord
+import androidx.health.connect.client.records.WeightRecord
+import androidx.health.connect.client.records.WheelchairPushesRecord
+import androidx.health.connect.client.records.metadata.DataOrigin
+import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.units.BloodGlucose
+import androidx.health.connect.client.units.Energy
+import androidx.health.connect.client.units.Length
+import androidx.health.connect.client.units.Mass
+import androidx.health.connect.client.units.Percentage
+import androidx.health.connect.client.units.Power
+import androidx.health.connect.client.units.Pressure
+import androidx.health.connect.client.units.Temperature
+import androidx.health.connect.client.units.Velocity
+import androidx.health.connect.client.units.Volume
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Correspondence
+import com.google.common.truth.Truth.assertThat
+import java.time.Instant
+import java.time.ZoneOffset
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+@SmallTest
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+// Comment the SDK suppress to run on emulators lower than U.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+class RecordConvertersTest {
+
+ private val tolerance = 1.0e-9
+
+ @Test
+ fun toPlatformRecordClass_supportsAllRecordTypes() {
+ RECORD_CLASSES.forEach { assertThat(it.toPlatformRecordClass()).isNotNull() }
+ }
+
+ @Test
+ fun stepsRecordClass_convertToPlatform() {
+ val stepsSdkClass = StepsRecord::class
+ val stepsPlatformClass = PlatformStepsRecord::class.java
+ assertThat(stepsSdkClass.toPlatformRecordClass()).isEqualTo(stepsPlatformClass)
+ }
+
+ @Test
+ fun activeCaloriesBurnedRecord_convertToPlatform() {
+ val platformActiveCaloriesBurned =
+ ActiveCaloriesBurnedRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA,
+ energy = Energy.calories(200.0),
+ )
+ .toPlatformRecord() as PlatformActiveCaloriesBurnedRecord
+
+ assertPlatformRecord(platformActiveCaloriesBurned) {
+ assertThat(energy).isEqualTo(PlatformEnergy.fromCalories(200.0))
+ }
+ }
+
+ @Test
+ fun basalBodyTemperatureRecord_convertToPlatform() {
+ val platformBasalBodyTemperature =
+ BasalBodyTemperatureRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ temperature = Temperature.celsius(37.0),
+ measurementLocation =
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_FINGER
+ )
+ .toPlatformRecord() as PlatformBasalBodyTemperatureRecord
+
+ assertPlatformRecord(platformBasalBodyTemperature) {
+ assertThat(temperature).isEqualTo(PlatformTemperature.fromCelsius(37.0))
+ assertThat(measurementLocation)
+ .isEqualTo(PlatformBodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_FINGER)
+ }
+ }
+
+ @Test
+ fun basalMetabolicRateRecord_convertToPlatform() {
+ val platformBasalMetabolicRate =
+ BasalMetabolicRateRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ basalMetabolicRate = Power.watts(300.0),
+ )
+ .toPlatformRecord() as PlatformBasalMetabolicRateRecord
+
+ assertPlatformRecord(platformBasalMetabolicRate) {
+ assertThat(basalMetabolicRate).isEqualTo(PlatformPower.fromWatts(300.0))
+ }
+ }
+
+ @Test
+ fun bloodGlucoseRecord_convertToPlatform() {
+ val platformBloodGlucose =
+ BloodGlucoseRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ level = BloodGlucose.millimolesPerLiter(34.0),
+ specimenSource = BloodGlucoseRecord.SPECIMEN_SOURCE_TEARS,
+ mealType = MealType.MEAL_TYPE_BREAKFAST,
+ relationToMeal = BloodGlucoseRecord.RELATION_TO_MEAL_AFTER_MEAL,
+ )
+ .toPlatformRecord() as PlatformBloodGlucoseRecord
+
+ assertPlatformRecord(platformBloodGlucose) {
+ assertThat(level).isEqualTo(PlatformBloodGlucose.fromMillimolesPerLiter(34.0))
+ assertThat(specimenSource)
+ .isEqualTo(PlatformBloodGlucoseSpecimenSource.SPECIMEN_SOURCE_TEARS)
+ assertThat(mealType).isEqualTo(PlatformMealType.MEAL_TYPE_BREAKFAST)
+ assertThat(relationToMeal)
+ .isEqualTo(PlatformBloodGlucoseRelationToMealType.RELATION_TO_MEAL_AFTER_MEAL)
+ }
+ }
+
+ @Test
+ fun bloodPressureRecord_convertToPlatform() {
+ val platformBloodPressure =
+ BloodPressureRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ systolic = Pressure.millimetersOfMercury(23.0),
+ diastolic = Pressure.millimetersOfMercury(24.0),
+ bodyPosition = BloodPressureRecord.BODY_POSITION_STANDING_UP,
+ measurementLocation = BloodPressureRecord.MEASUREMENT_LOCATION_LEFT_WRIST,
+ )
+ .toPlatformRecord() as PlatformBloodPressureRecord
+
+ assertPlatformRecord(platformBloodPressure) {
+ assertThat(systolic).isEqualTo(PlatformPressure.fromMillimetersOfMercury(23.0))
+ assertThat(diastolic).isEqualTo(PlatformPressure.fromMillimetersOfMercury(24.0))
+ assertThat(bodyPosition)
+ .isEqualTo(PlatformBloodPressureBodyPosition.BODY_POSITION_STANDING_UP)
+ assertThat(measurementLocation)
+ .isEqualTo(
+ PlatformBloodPressureMeasurementLocation
+ .BLOOD_PRESSURE_MEASUREMENT_LOCATION_LEFT_WRIST
+ )
+ }
+ }
+
+ @Test
+ fun bodyFatRecord_convertToPlatform() {
+ val platformBodyFat =
+ BodyFatRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ percentage = Percentage(99.0),
+ )
+ .toPlatformRecord() as PlatformBodyFatRecord
+
+ assertPlatformRecord(platformBodyFat) {
+ assertThat(percentage).isEqualTo(PlatformPercentage.fromValue(99.0))
+ }
+ }
+
+ @Test
+ fun bodyTemperatureRecord_convertToPlatform() {
+ val platformBodyTemperature =
+ BodyTemperatureRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ temperature = Temperature.celsius(30.0),
+ measurementLocation =
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_ARMPIT,
+ )
+ .toPlatformRecord() as PlatformBodyTemperatureRecord
+
+ assertPlatformRecord(platformBodyTemperature) {
+ PlatformTemperature.fromCelsius(30.0)
+ assertThat(measurementLocation)
+ .isEqualTo(PlatformBodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_ARMPIT)
+ }
+ }
+
+ @Test
+ fun bodyWaterMassRecord_convertToPlatform() {
+ val platformBodyWaterMass =
+ BodyWaterMassRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ mass = Mass.grams(40.0),
+ )
+ .toPlatformRecord() as PlatformBodyWaterMassRecord
+
+ assertPlatformRecord(platformBodyWaterMass) {
+ assertThat(bodyWaterMass).isEqualTo(PlatformMass.fromGrams(40.0))
+ }
+ }
+
+ @Test
+ fun boneMassRecord_convertToPlatform() {
+ val platformBoneMass =
+ BoneMassRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ mass = Mass.grams(5.0),
+ )
+ .toPlatformRecord() as PlatformBoneMassRecord
+
+ assertPlatformRecord(platformBoneMass) {
+ assertThat(mass).isEqualTo(PlatformMass.fromGrams(5.0))
+ }
+ }
+
+ @Test
+ fun cervicalMucusRecord_convertToPlatform() {
+ val platformCervicalMucus =
+ CervicalMucusRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ appearance = CervicalMucusRecord.APPEARANCE_CREAMY,
+ sensation = CervicalMucusRecord.SENSATION_LIGHT,
+ )
+ .toPlatformRecord() as PlatformCervicalMucusRecord
+
+ assertPlatformRecord(platformCervicalMucus) {
+ assertThat(appearance).isEqualTo(PlatformCervicalMucusAppearance.APPEARANCE_CREAMY)
+ assertThat(sensation).isEqualTo(PlatformCervicalMucusSensation.SENSATION_LIGHT)
+ }
+ }
+
+ @Test
+ fun cyclingPedalingCadenceRecord_convertToPlatform() {
+ val platformCyclingPedalingCadence =
+ CyclingPedalingCadenceRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA,
+ samples =
+ listOf(
+ CyclingPedalingCadenceRecord.Sample(START_TIME, 3.0),
+ CyclingPedalingCadenceRecord.Sample(END_TIME, 9.0)
+ ),
+ )
+ .toPlatformRecord() as PlatformCyclingPedalingCadenceRecord
+
+ assertPlatformRecord(platformCyclingPedalingCadence) {
+ assertThat(samples)
+ .comparingElementsUsing(
+ Correspondence.from<
+ PlatformCyclingPedalingCadenceSample, PlatformCyclingPedalingCadenceSample
+ >(
+ { actual, expected ->
+ actual!!.revolutionsPerMinute == expected!!.revolutionsPerMinute &&
+ actual.time == expected.time
+ },
+ "has same RPM and same time as"
+ )
+ )
+ .containsExactly(
+ PlatformCyclingPedalingCadenceSample(3.0, START_TIME),
+ PlatformCyclingPedalingCadenceSample(9.0, END_TIME)
+ )
+ }
+ }
+
+ @Test
+ fun distanceRecord_convertToPlatform() {
+ val platformDistance =
+ DistanceRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA,
+ distance = Length.meters(50.0),
+ )
+ .toPlatformRecord() as PlatformDistanceRecord
+
+ assertPlatformRecord(platformDistance) {
+ assertThat(distance).isEqualTo(PlatformLength.fromMeters(50.0))
+ }
+ }
+
+ @Test
+ fun elevationGainedRecord_convertToPlatform() {
+ val platformElevationGained =
+ ElevationGainedRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA,
+ elevation = Length.meters(10.0),
+ )
+ .toPlatformRecord() as PlatformElevationGainedRecord
+
+ assertPlatformRecord(platformElevationGained) {
+ assertThat(elevation).isEqualTo(PlatformLength.fromMeters(10.0))
+ }
+ }
+
+ @Test
+ fun exerciseSessionRecord_convertToPlatform() {
+ val platformExerciseSession =
+ ExerciseSessionRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA,
+ exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_BASKETBALL,
+ title = "NBA finals",
+ notes = "Best team won",
+ )
+ .toPlatformRecord() as PlatformExerciseSessionRecord
+
+ assertPlatformRecord(platformExerciseSession) {
+ assertThat(title).isEqualTo("NBA finals")
+ assertThat(notes).isEqualTo("Best team won")
+ assertThat(exerciseType)
+ .isEqualTo(PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_BASKETBALL)
+ }
+ }
+
+ @Test
+ fun floorsClimbedRecord_convertToPlatform() {
+ val platformFloorsClimbed =
+ FloorsClimbedRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA,
+ floors = 3.9,
+ )
+ .toPlatformRecord() as PlatformFloorsClimbedRecord
+
+ assertPlatformRecord(platformFloorsClimbed) { assertThat(floors).isEqualTo(3.9) }
+ }
+
+ @Test
+ fun heartRateRecord_convertToPlatform() {
+ val heartRate =
+ HeartRateRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA,
+ samples =
+ listOf(
+ HeartRateRecord.Sample(Instant.ofEpochMilli(1234L), 55L),
+ HeartRateRecord.Sample(Instant.ofEpochMilli(5678L), 57L)
+ )
+ )
+
+ val platformHeartRate = heartRate.toPlatformRecord() as PlatformHeartRateRecord
+
+ assertPlatformRecord(platformHeartRate) {
+ assertThat(samples)
+ .comparingElementsUsing(
+ Correspondence.from<PlatformHeartRateSample, PlatformHeartRateSample>(
+ { actual, expected ->
+ actual!!.beatsPerMinute == expected!!.beatsPerMinute &&
+ actual.time == expected.time
+ },
+ "has same BPM and same time as"
+ )
+ )
+ .containsExactly(
+ PlatformHeartRateSample(55L, Instant.ofEpochMilli(1234L)),
+ PlatformHeartRateSample(57L, Instant.ofEpochMilli(5678L))
+ )
+ }
+ }
+ @Test
+ fun heartRateVariabilityRmssdRecord_convertToPlatform() {
+ val platformHeartRateVariabilityRmssd =
+ HeartRateVariabilityRmssdRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ heartRateVariabilityMillis = 1.0,
+ )
+ .toPlatformRecord() as PlatformHeartRateVariabilityRmssdRecord
+
+ assertPlatformRecord(platformHeartRateVariabilityRmssd) {
+ assertThat(heartRateVariabilityMillis).isEqualTo(1.0)
+ }
+ }
+
+ @Test
+ fun heightRecord_convertToPlatform() {
+ val platformHeight =
+ HeightRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ height = Length.meters(1.8),
+ )
+ .toPlatformRecord() as PlatformHeightRecord
+
+ assertPlatformRecord(platformHeight) {
+ assertThat(height).isEqualTo(PlatformLength.fromMeters(1.8))
+ }
+ }
+
+ @Test
+ fun hydrationRecord_convertToPlatform() {
+ val platformHydration =
+ HydrationRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA,
+ volume = Volume.liters(90.0),
+ )
+ .toPlatformRecord() as PlatformHydrationRecord
+
+ assertPlatformRecord(platformHydration) {
+ assertThat(volume).isEqualTo(PlatformVolume.fromLiters(90.0))
+ }
+ }
+
+ @Test
+ fun intermenstrualBleedingRecord_convertToPlatform() {
+ val platformIntermenstrualBleeding =
+ IntermenstrualBleedingRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ )
+ .toPlatformRecord() as PlatformIntermenstrualBleedingRecord
+
+ assertPlatformRecord(platformIntermenstrualBleeding)
+ }
+
+ @Test
+ fun leanBodyMassRecord_convertToPlatform() {
+ val platformLeanBodyMass =
+ LeanBodyMassRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ mass = Mass.grams(21.3),
+ )
+ .toPlatformRecord() as PlatformLeanBodyMassRecord
+
+ assertPlatformRecord(platformLeanBodyMass) {
+ assertThat(mass).isEqualTo(PlatformMass.fromGrams(21.3))
+ }
+ }
+
+ @Test
+ fun menstruationFlowRecord_convertToPlatform() {
+ val platformMenstruationFlow =
+ MenstruationFlowRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ flow = MenstruationFlowRecord.FLOW_MEDIUM,
+ )
+ .toPlatformRecord() as PlatformMenstruationFlowRecord
+
+ assertPlatformRecord(platformMenstruationFlow) {
+ assertThat(flow).isEqualTo(PlatformMenstruationFlowType.FLOW_MEDIUM)
+ }
+ }
+
+ @Test
+ fun menstruationPeriodRecord_convertToPlatform() {
+ val platformMenstruationPeriod =
+ MenstruationPeriodRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA
+ )
+ .toPlatformRecord() as PlatformMenstruationPeriodRecord
+
+ assertPlatformRecord(platformMenstruationPeriod)
+ }
+
+ @Test
+ fun nutritionRecord_convertToPlatform() {
+ val nutrition =
+ NutritionRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA,
+ caffeine = Mass.grams(20.0),
+ energy = Energy.calories(300.0)
+ )
+
+ val platformNutrition = nutrition.toPlatformRecord() as PlatformNutritionRecord
+
+ assertPlatformRecord(platformNutrition) {
+ assertThat(caffeine!!.inGrams).isWithin(tolerance).of(20.0)
+ assertThat(energy!!.inCalories).isWithin(tolerance).of(300.0)
+ }
+ }
+
+ @Test
+ fun ovulationTestRecord_convertToPlatform() {
+ val platformOvulationTest =
+ OvulationTestRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ result = OvulationTestRecord.RESULT_POSITIVE,
+ )
+ .toPlatformRecord() as PlatformOvulationTestRecord
+
+ assertPlatformRecord(platformOvulationTest) {
+ assertThat(result).isEqualTo(PlatformOvulationTestResult.RESULT_POSITIVE)
+ }
+ }
+
+ @Test
+ fun oxygenSaturationRecord_convertToPlatform() {
+ val platformOxygenSaturation =
+ OxygenSaturationRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ percentage = Percentage(15.0),
+ )
+ .toPlatformRecord() as PlatformOxygenSaturationRecord
+
+ assertPlatformRecord(platformOxygenSaturation) {
+ assertThat(percentage).isEqualTo(PlatformPercentage.fromValue(15.0))
+ }
+ }
+
+ @Test
+ fun powerRecord_convertToPlatform() {
+ val platformPowerRecord =
+ PowerRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA,
+ samples = listOf(PowerRecord.Sample(START_TIME, Power.watts(300.0))),
+ )
+ .toPlatformRecord() as PlatformPowerRecord
+
+ assertPlatformRecord(platformPowerRecord) {
+ assertThat(samples)
+ .containsExactly(
+ PlatformPowerRecordSample(PlatformPower.fromWatts(300.0), START_TIME)
+ )
+ }
+ }
+
+ @Test
+ fun respiratoryRateRecord_convertToPlatform() {
+ val platformRespiratoryRate =
+ RespiratoryRateRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ rate = 12.0,
+ )
+ .toPlatformRecord() as PlatformRespiratoryRateRecord
+
+ assertPlatformRecord(platformRespiratoryRate) { assertThat(rate).isEqualTo(12.0) }
+ }
+
+ @Test
+ fun restingHeartRateRecord_convertToPlatform() {
+ val platformRestingHeartRate =
+ RestingHeartRateRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ beatsPerMinute = 57L,
+ )
+ .toPlatformRecord() as PlatformRestingHeartRateRecord
+
+ assertPlatformRecord(platformRestingHeartRate) { assertThat(beatsPerMinute).isEqualTo(57L) }
+ }
+
+ @Test
+ fun sexualActivityRecord_convertToPlatform() {
+ val platformSexualActivity =
+ SexualActivityRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ protectionUsed = SexualActivityRecord.PROTECTION_USED_PROTECTED,
+ )
+ .toPlatformRecord() as PlatformSexualActivityRecord
+
+ assertPlatformRecord(platformSexualActivity) {
+ assertThat(protectionUsed)
+ .isEqualTo(PlatformSexualActivityProtectionUsed.PROTECTION_USED_PROTECTED)
+ }
+ }
+
+ @Test
+ fun sleepSessionRecord_convertToPlatform() {
+ val platformSleepSession =
+ SleepSessionRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA,
+ title = "Night night",
+ notes = "Many dreams",
+ )
+ .toPlatformRecord() as PlatformSleepSessionRecord
+
+ assertPlatformRecord(platformSleepSession) {
+ assertThat(title).isEqualTo("Night night")
+ assertThat(notes).isEqualTo("Many dreams")
+ }
+ }
+
+ @Test
+ fun speedRecord_convertToPlatform() {
+ val platformSpeed =
+ SpeedRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA,
+ samples = listOf(SpeedRecord.Sample(END_TIME, Velocity.metersPerSecond(3.0))),
+ )
+ .toPlatformRecord() as PlatformSpeedRecord
+
+ assertPlatformRecord(platformSpeed) {
+ assertThat(samples)
+ .comparingElementsUsing(
+ Correspondence.from<PlatformSpeedSample, PlatformSpeedSample>(
+ { actual, expected ->
+ actual!!.speed.inMetersPerSecond ==
+ expected!!.speed.inMetersPerSecond && actual.time == expected.time
+ },
+ "has same speed and same time as"
+ )
+ )
+ .containsExactly(
+ PlatformSpeedSample(PlatformVelocity.fromMetersPerSecond(3.0), END_TIME)
+ )
+ }
+ }
+
+ @Test
+ fun stepsRecord_convertToPlatform() {
+ val platformSteps =
+ StepsRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA,
+ count = 10,
+ )
+ .toPlatformRecord() as PlatformStepsRecord
+
+ assertPlatformRecord(platformSteps) { assertThat(count).isEqualTo(10) }
+ }
+
+ @Test
+ fun stepsCadenceRecord_convertToPlatform() {
+ val platformStepsCadence =
+ StepsCadenceRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA,
+ samples = listOf(StepsCadenceRecord.Sample(END_TIME, 99.0)),
+ )
+ .toPlatformRecord() as PlatformStepsCadenceRecord
+
+ assertPlatformRecord(platformStepsCadence) {
+ assertThat(samples)
+ .comparingElementsUsing(
+ Correspondence.from<PlatformStepsCadenceSample, PlatformStepsCadenceSample>(
+ { actual, expected ->
+ actual!!.rate == expected!!.rate && actual.time == expected.time
+ },
+ "has same rate and same time as"
+ )
+ )
+ .containsExactly(PlatformStepsCadenceSample(99.0, END_TIME))
+ }
+ }
+
+ @Test
+ fun totalCaloriesBurnedRecord_convertToPlatform() {
+ val platformTotalCaloriesBurned =
+ TotalCaloriesBurnedRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA,
+ energy = Energy.calories(100.0),
+ )
+ .toPlatformRecord() as PlatformTotalCaloriesBurnedRecord
+
+ assertPlatformRecord(platformTotalCaloriesBurned) {
+ assertThat(energy).isEqualTo(PlatformEnergy.fromCalories(100.0))
+ }
+ }
+
+ @Test
+ fun vo2MaxRecord_convertToPlatform() {
+ val platformVo2Max =
+ Vo2MaxRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ vo2MillilitersPerMinuteKilogram = 5.0,
+ measurementMethod = Vo2MaxRecord.MEASUREMENT_METHOD_MULTISTAGE_FITNESS_TEST
+ )
+ .toPlatformRecord() as PlatformVo2MaxRecord
+
+ assertPlatformRecord(platformVo2Max) {
+ assertThat(vo2MillilitersPerMinuteKilogram).isEqualTo(5.0)
+ assertThat(measurementMethod)
+ .isEqualTo(
+ PlatformVo2MaxMeasurementMethod.MEASUREMENT_METHOD_MULTISTAGE_FITNESS_TEST
+ )
+ }
+ }
+
+ @Test
+ fun weightRecord_convertToPlatform() {
+ val platformWeight =
+ WeightRecord(
+ time = TIME,
+ zoneOffset = ZONE_OFFSET,
+ metadata = METADATA,
+ weight = Mass.grams(100.0),
+ )
+ .toPlatformRecord() as PlatformWeightRecord
+
+ assertPlatformRecord(platformWeight) {
+ assertThat(weight).isEqualTo(PlatformMass.fromGrams(100.0))
+ }
+ }
+
+ @Test
+ fun wheelChairPushesRecord_convertToPlatform() {
+ val platformWheelchairPushes =
+ WheelchairPushesRecord(
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = METADATA,
+ count = 10,
+ )
+ .toPlatformRecord() as PlatformWheelchairPushesRecord
+
+ assertPlatformRecord(platformWheelchairPushes) { assertThat(count).isEqualTo(10) }
+ }
+
+ @Test
+ fun activeCaloriesBurnedRecord_convertToSdk() {
+ val sdkActiveCaloriesBurned =
+ PlatformActiveCaloriesBurnedRecordBuilder(
+ PLATFORM_METADATA,
+ START_TIME,
+ END_TIME,
+ PlatformEnergy.fromCalories(300.0)
+ )
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as ActiveCaloriesBurnedRecord
+
+ assertSdkRecord(sdkActiveCaloriesBurned) {
+ assertThat(energy).isEqualTo(Energy.calories(300.0))
+ }
+ }
+
+ @Test
+ fun basalBodyTemperatureRecord_convertToSdk() {
+ val sdkBasalBodyTemperature =
+ PlatformBasalBodyTemperatureRecordBuilder(
+ PLATFORM_METADATA,
+ TIME,
+ PlatformBodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_RECTUM,
+ PlatformTemperature.fromCelsius(37.0)
+ )
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as BasalBodyTemperatureRecord
+
+ assertSdkRecord(sdkBasalBodyTemperature) {
+ assertThat(measurementLocation)
+ .isEqualTo(BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_RECTUM)
+ assertThat(temperature).isEqualTo(Temperature.celsius(37.0))
+ }
+ }
+
+ @Test
+ fun basalMetabolicRateRecord_convertToSdk() {
+ val sdkBasalMetabolicRate =
+ PlatformBasalMetabolicRateRecordBuilder(
+ PLATFORM_METADATA,
+ TIME,
+ PlatformPower.fromWatts(100.0)
+ )
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as BasalMetabolicRateRecord
+
+ assertSdkRecord(sdkBasalMetabolicRate) {
+ assertThat(basalMetabolicRate).isEqualTo(Power.watts(100.0))
+ }
+ }
+
+ @Test
+ fun bloodGlucoseRecord_convertToSdk() {
+ val sdkBloodGlucose =
+ PlatformBloodGlucoseRecordBuilder(
+ PLATFORM_METADATA,
+ TIME,
+ PlatformBloodGlucoseSpecimenSource.SPECIMEN_SOURCE_TEARS,
+ PlatformBloodGlucose.fromMillimolesPerLiter(10.2),
+ PlatformBloodGlucoseRelationToMealType.RELATION_TO_MEAL_FASTING,
+ PlatformMealType.MEAL_TYPE_SNACK
+ )
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as BloodGlucoseRecord
+
+ assertSdkRecord(sdkBloodGlucose) {
+ assertThat(level).isEqualTo(BloodGlucose.millimolesPerLiter(10.2))
+ assertThat(specimenSource).isEqualTo(BloodGlucoseRecord.SPECIMEN_SOURCE_TEARS)
+ assertThat(mealType).isEqualTo(MealType.MEAL_TYPE_SNACK)
+ assertThat(relationToMeal).isEqualTo(BloodGlucoseRecord.RELATION_TO_MEAL_FASTING)
+ }
+ }
+
+ @Test
+ fun bloodPressureRecord_convertToSdk() {
+ val sdkBloodPressure =
+ PlatformBloodPressureRecordBuilder(
+ PLATFORM_METADATA,
+ TIME,
+ PlatformBloodPressureMeasurementLocation
+ .BLOOD_PRESSURE_MEASUREMENT_LOCATION_LEFT_WRIST,
+ PlatformPressure.fromMillimetersOfMercury(20.0),
+ PlatformPressure.fromMillimetersOfMercury(15.0),
+ PlatformBloodPressureBodyPosition.BODY_POSITION_STANDING_UP
+ )
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as BloodPressureRecord
+
+ assertSdkRecord(sdkBloodPressure) {
+ assertThat(measurementLocation)
+ .isEqualTo(BloodPressureRecord.MEASUREMENT_LOCATION_LEFT_WRIST)
+ assertThat(systolic).isEqualTo(Pressure.millimetersOfMercury(20.0))
+ assertThat(diastolic).isEqualTo(Pressure.millimetersOfMercury(15.0))
+ assertThat(bodyPosition).isEqualTo(BloodPressureRecord.BODY_POSITION_STANDING_UP)
+ }
+ }
+
+ @Test
+ fun bodyFatRecord_convertToSdk() {
+ val sdkBodyFat =
+ PlatformBodyFatRecordBuilder(
+ PLATFORM_METADATA,
+ TIME,
+ PlatformPercentage.fromValue(18.0)
+ )
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as BodyFatRecord
+
+ assertSdkRecord(sdkBodyFat) { assertThat(percentage).isEqualTo(Percentage(18.0)) }
+ }
+
+ @Test
+ fun bodyTemperatureRecord_convertToSdk() {
+ val sdkBodyTemperature =
+ PlatformBodyTemperatureRecordBuilder(
+ PLATFORM_METADATA,
+ TIME,
+ PlatformBodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_WRIST,
+ PlatformTemperature.fromCelsius(27.0)
+ )
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as BodyTemperatureRecord
+
+ assertSdkRecord(sdkBodyTemperature) {
+ assertThat(measurementLocation)
+ .isEqualTo(BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_WRIST)
+ assertThat(temperature).isEqualTo(Temperature.celsius(27.0))
+ }
+ }
+
+ @Test
+ fun bodyWaterMassRecord_convertToSdk() {
+ val sdkBodyWaterMass =
+ PlatformBodyWaterMassRecordBuilder(
+ PLATFORM_METADATA,
+ TIME,
+ PlatformMass.fromGrams(12.0)
+ )
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as BodyWaterMassRecord
+
+ assertSdkRecord(sdkBodyWaterMass) { assertThat(mass).isEqualTo(Mass.grams(12.0)) }
+ }
+
+ @Test
+ fun boneMassRecord_convertToSdk() {
+ val sdkBoneMass =
+ PlatformBoneMassRecordBuilder(PLATFORM_METADATA, TIME, PlatformMass.fromGrams(73.0))
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as BoneMassRecord
+
+ assertSdkRecord(sdkBoneMass) { assertThat(mass).isEqualTo(Mass.grams(73.0)) }
+ }
+
+ @Test
+ fun cervicalMucusRecord_convertToSdk() {
+ val sdkCervicalMucus =
+ PlatformCervicalMucusRecordBuilder(
+ PLATFORM_METADATA,
+ TIME,
+ PlatformCervicalMucusSensation.SENSATION_HEAVY,
+ PlatformCervicalMucusAppearance.APPEARANCE_DRY
+ )
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as CervicalMucusRecord
+
+ assertSdkRecord(sdkCervicalMucus) {
+ assertThat(sensation).isEqualTo(CervicalMucusRecord.SENSATION_HEAVY)
+ assertThat(appearance).isEqualTo(CervicalMucusRecord.APPEARANCE_DRY)
+ }
+ }
+
+ @Test
+ fun cyclingPedalingCadenceRecord_convertToSdk() {
+ val sdkCyclingPedalingCadence =
+ PlatformCyclingPedalingCadenceRecordBuilder(
+ PLATFORM_METADATA,
+ START_TIME,
+ END_TIME,
+ listOf(PlatformCyclingPedalingCadenceSample(23.0, END_TIME))
+ )
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as CyclingPedalingCadenceRecord
+
+ assertSdkRecord(sdkCyclingPedalingCadence) {
+ assertThat(samples).containsExactly(CyclingPedalingCadenceRecord.Sample(END_TIME, 23.0))
+ }
+ }
+
+ @Test
+ fun distanceRecord_convertToSdk() {
+ val sdkDistance =
+ PlatformDistanceRecordBuilder(
+ PLATFORM_METADATA,
+ START_TIME,
+ END_TIME,
+ PlatformLength.fromMeters(500.0)
+ )
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as DistanceRecord
+
+ assertSdkRecord(sdkDistance) { assertThat(distance).isEqualTo(Length.meters(500.0)) }
+ }
+
+ @Test
+ fun elevationGainedRecord_convertToSdk() {
+ val sdkElevationGained =
+ PlatformElevationGainedRecordBuilder(
+ PLATFORM_METADATA,
+ START_TIME,
+ END_TIME,
+ PlatformLength.fromMeters(10.0)
+ )
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as ElevationGainedRecord
+
+ assertSdkRecord(sdkElevationGained) { assertThat(elevation).isEqualTo(Length.meters(10.0)) }
+ }
+
+ @Test
+ fun exerciseSessionRecord_convertToSdk() {
+ val sdkExerciseSession =
+ PlatformExerciseSessionRecordBuilder(
+ PLATFORM_METADATA,
+ START_TIME,
+ END_TIME,
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_VOLLEYBALL
+ )
+ .setTitle("Training game")
+ .setNotes("Improve jump serve")
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as ExerciseSessionRecord
+
+ assertSdkRecord(sdkExerciseSession) {
+ assertThat(title).isEqualTo("Training game")
+ assertThat(notes).isEqualTo("Improve jump serve")
+ assertThat(exerciseType).isEqualTo(ExerciseSessionRecord.EXERCISE_TYPE_VOLLEYBALL)
+ }
+ }
+
+ @Test
+ fun floorsClimbedRecord_convertToSdk() {
+ val sdkFloorsClimbed =
+ PlatformFloorsClimbedRecordBuilder(PLATFORM_METADATA, START_TIME, END_TIME, 10.0)
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as FloorsClimbedRecord
+
+ assertSdkRecord(sdkFloorsClimbed) { assertThat(floors).isEqualTo(10.0) }
+ }
+
+ @Test
+ fun heartRateRecord_convertToSdk() {
+ val sdkHeartRate =
+ PlatformHeartRateRecordBuilder(
+ PLATFORM_METADATA,
+ START_TIME,
+ END_TIME,
+ listOf(PlatformHeartRateSample(83, START_TIME))
+ )
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as HeartRateRecord
+
+ assertSdkRecord(sdkHeartRate) {
+ assertThat(samples).containsExactly(HeartRateRecord.Sample(START_TIME, 83))
+ }
+ }
+
+ @Test
+ fun heartRateVariabilityRmssdRecord_convertToSdk() {
+ val sdkHeartRateVariabilityRmssd =
+ PlatformHeartRateVariabilityRmssdRecordBuilder(PLATFORM_METADATA, TIME, 0.6)
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as HeartRateVariabilityRmssdRecord
+
+ assertSdkRecord(sdkHeartRateVariabilityRmssd) {
+ assertThat(heartRateVariabilityMillis).isEqualTo(0.6)
+ }
+ }
+
+ @Test
+ fun heightRecord_convertToSdk() {
+ val sdkHeight =
+ PlatformHeightRecordBuilder(PLATFORM_METADATA, TIME, PlatformLength.fromMeters(1.7))
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as HeightRecord
+
+ assertSdkRecord(sdkHeight) { assertThat(height).isEqualTo(Length.meters(1.7)) }
+ }
+
+ @Test
+ fun hydrationRecord_convertToSdk() {
+ val sdkHydration =
+ PlatformHydrationRecordBuilder(
+ PLATFORM_METADATA,
+ START_TIME,
+ END_TIME,
+ PlatformVolume.fromLiters(90.0)
+ )
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as HydrationRecord
+
+ assertSdkRecord(sdkHydration) { assertThat(volume).isEqualTo(Volume.liters(90.0)) }
+ }
+
+ @Test
+ fun intermenstrualBleedingRecord_convertToSdk() {
+ val sdkIntermenstrualBleeding =
+ PlatformIntermenstrualBleedingRecordBuilder(
+ PLATFORM_METADATA,
+ TIME,
+ )
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as IntermenstrualBleedingRecord
+
+ assertSdkRecord(sdkIntermenstrualBleeding)
+ }
+
+ @Test
+ fun leanBodyMassRecord_convertToSdk() {
+ val sdkLeanBodyMass =
+ PlatformLeanBodyMassRecordBuilder(PLATFORM_METADATA, TIME, PlatformMass.fromGrams(9.0))
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as LeanBodyMassRecord
+
+ assertSdkRecord(sdkLeanBodyMass) { assertThat(mass).isEqualTo(Mass.grams(9.0)) }
+ }
+
+ @Test
+ fun menstruationFlowRecord_convertToSdk() {
+ val sdkMenstruationFlow =
+ PlatformMenstruationFlowRecordBuilder(
+ PLATFORM_METADATA,
+ TIME,
+ PlatformMenstruationFlowType.FLOW_MEDIUM
+ )
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as MenstruationFlowRecord
+
+ assertSdkRecord(sdkMenstruationFlow) {
+ assertThat(flow).isEqualTo(MenstruationFlowRecord.FLOW_MEDIUM)
+ }
+ }
+
+ @Test
+ fun menstruationPeriodRecord_convertToSdk() {
+ val sdkMenstruationPeriod =
+ PlatformMenstruationPeriodRecordBuilder(PLATFORM_METADATA, START_TIME, END_TIME)
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as MenstruationPeriodRecord
+
+ assertSdkRecord(sdkMenstruationPeriod)
+ }
+
+ @Test
+ fun nutritionRecord_convertToSdk() {
+ val sdkNutrition =
+ PlatformNutritionRecordBuilder(PLATFORM_METADATA, START_TIME, END_TIME)
+ .setMealName("Cheat meal")
+ .setMealType(PlatformMealType.MEAL_TYPE_DINNER)
+ .setChromium(PlatformMass.fromGrams(0.01))
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as NutritionRecord
+
+ assertSdkRecord(sdkNutrition) {
+ assertThat(name).isEqualTo("Cheat meal")
+ assertThat(mealType).isEqualTo(MealType.MEAL_TYPE_DINNER)
+ assertThat(chromium).isEqualTo(Mass.grams(0.01))
+ }
+ }
+
+ @Test
+ fun ovulationTestRecord_convertToSdk() {
+ val sdkOvulationTest =
+ PlatformOvulationTestRecordBuilder(
+ PLATFORM_METADATA,
+ TIME,
+ PlatformOvulationTestResult.RESULT_NEGATIVE
+ )
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as OvulationTestRecord
+
+ assertSdkRecord(sdkOvulationTest) {
+ assertThat(result).isEqualTo(OvulationTestRecord.RESULT_NEGATIVE)
+ }
+ }
+
+ @Test
+ fun oxygenSaturationRecord_convertToSdk() {
+ val sdkOxygenSaturation =
+ PlatformOxygenSaturationRecordBuilder(
+ PLATFORM_METADATA,
+ TIME,
+ PlatformPercentage.fromValue(21.0)
+ )
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as OxygenSaturationRecord
+
+ assertSdkRecord(sdkOxygenSaturation) { assertThat(percentage).isEqualTo(Percentage(21.0)) }
+ }
+
+ @Test
+ fun powerRecord_convertToSdk() {
+ val sdkPower =
+ PlatformPowerRecordBuilder(
+ PLATFORM_METADATA,
+ START_TIME,
+ END_TIME,
+ listOf(PlatformPowerRecordSample(PlatformPower.fromWatts(300.0), START_TIME))
+ )
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as PowerRecord
+
+ assertSdkRecord(sdkPower) {
+ assertThat(samples).containsExactly(PowerRecord.Sample(START_TIME, Power.watts(300.0)))
+ }
+ }
+
+ @Test
+ fun respiratoryRateRecord_convertToSdk() {
+ val sdkRespiratoryRate =
+ PlatformRespiratoryRateRecordBuilder(PLATFORM_METADATA, TIME, 12.0)
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as RespiratoryRateRecord
+
+ assertSdkRecord(sdkRespiratoryRate) { assertThat(rate).isEqualTo(12.0) }
+ }
+
+ @Test
+ fun restingHeartRateRecord_convertToSdk() {
+ val sdkRestingHeartRate =
+ PlatformRestingHeartRateRecordBuilder(PLATFORM_METADATA, TIME, 37)
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as RestingHeartRateRecord
+
+ assertSdkRecord(sdkRestingHeartRate) { assertThat(beatsPerMinute).isEqualTo(37) }
+ }
+
+ @Test
+ fun sexualActivityRecord_convertToSdk() {
+ val sdkSexualActivity =
+ PlatformSexualActivityRecordBuilder(
+ PLATFORM_METADATA,
+ TIME,
+ PlatformSexualActivityProtectionUsed.PROTECTION_USED_PROTECTED
+ )
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as SexualActivityRecord
+
+ assertSdkRecord(sdkSexualActivity) {
+ assertThat(protectionUsed).isEqualTo(SexualActivityRecord.PROTECTION_USED_PROTECTED)
+ }
+ }
+
+ @Test
+ fun sleepSessionRecord_convertToSdk() {
+ val sdkSleepSession =
+ PlatformSleepSessionRecordBuilder(PLATFORM_METADATA, START_TIME, END_TIME)
+ .setTitle("nap")
+ .setNotes("Afternoon reset")
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as SleepSessionRecord
+
+ assertSdkRecord(sdkSleepSession) {
+ assertThat(title).isEqualTo("nap")
+ assertThat(notes).isEqualTo("Afternoon reset")
+ }
+ }
+
+ @Test
+ fun speedRecord_convertToSdk() {
+ val sdkSpeed =
+ PlatformSpeedRecordBuilder(
+ PLATFORM_METADATA,
+ START_TIME,
+ END_TIME,
+ listOf(
+ PlatformSpeedSample(PlatformVelocity.fromMetersPerSecond(99.0), END_TIME)
+ )
+ )
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as SpeedRecord
+
+ assertSdkRecord(sdkSpeed) {
+ assertThat(samples)
+ .containsExactly(SpeedRecord.Sample(END_TIME, Velocity.metersPerSecond(99.0)))
+ }
+ }
+
+ @Test
+ fun stepsCadenceRecord_convertToSdk() {
+ val sdkStepsCadence =
+ PlatformStepsCadenceRecordBuilder(
+ PLATFORM_METADATA,
+ START_TIME,
+ END_TIME,
+ listOf(PlatformStepsCadenceSample(10.0, END_TIME))
+ )
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as StepsCadenceRecord
+
+ assertSdkRecord(sdkStepsCadence) {
+ assertThat(samples).containsExactly(StepsCadenceRecord.Sample(END_TIME, 10.0))
+ }
+ }
+
+ @Test
+ fun stepsRecord_convertToSdk() {
+ val sdkSteps =
+ PlatformStepsRecordBuilder(PLATFORM_METADATA, START_TIME, END_TIME, 10)
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as StepsRecord
+
+ assertSdkRecord(sdkSteps) { assertThat(count).isEqualTo(10) }
+ }
+
+ @Test
+ fun totalCaloriesBurnedRecord_convertToSdk() {
+ val sdkTotalCaloriesBurned =
+ PlatformTotalCaloriesBurnedRecordBuilder(
+ PLATFORM_METADATA,
+ START_TIME,
+ END_TIME,
+ PlatformEnergy.fromCalories(333.0)
+ )
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as TotalCaloriesBurnedRecord
+
+ assertSdkRecord(sdkTotalCaloriesBurned) {
+ assertThat(energy).isEqualTo(Energy.calories(333.0))
+ }
+ }
+
+ @Test
+ fun vo2MaxRecord_convertToSdk() {
+ val sdkVo2Max =
+ PlatformVo2MaxRecordBuilder(
+ PLATFORM_METADATA,
+ TIME,
+ PlatformVo2MaxMeasurementMethod.MEASUREMENT_METHOD_MULTISTAGE_FITNESS_TEST,
+ 13.0
+ )
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as Vo2MaxRecord
+
+ assertSdkRecord(sdkVo2Max) {
+ assertThat(measurementMethod)
+ .isEqualTo(Vo2MaxRecord.MEASUREMENT_METHOD_MULTISTAGE_FITNESS_TEST)
+ assertThat(vo2MillilitersPerMinuteKilogram).isEqualTo(13.0)
+ }
+ }
+
+ @Test
+ fun weightRecord_convertToSdk() {
+ val sdkWeight =
+ PlatformWeightRecordBuilder(PLATFORM_METADATA, TIME, PlatformMass.fromGrams(63.0))
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as WeightRecord
+
+ assertSdkRecord(sdkWeight) { assertThat(weight).isEqualTo(Mass.grams(63.0)) }
+ }
+
+ @Test
+ fun wheelChairPushesRecord_convertToSdk() {
+ val sdkWheelchairPushes =
+ PlatformWheelchairPushesRecordBuilder(PLATFORM_METADATA, START_TIME, END_TIME, 18)
+ .setStartZoneOffset(START_ZONE_OFFSET)
+ .setEndZoneOffset(END_ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as WheelchairPushesRecord
+
+ assertSdkRecord(sdkWheelchairPushes) { assertThat(count).isEqualTo(18) }
+ }
+
+ private fun <T : PlatformIntervalRecord> assertPlatformRecord(platformRecord: T) {
+ assertPlatformRecord(platformRecord) {}
+ }
+
+ private fun <T : PlatformIntervalRecord> assertPlatformRecord(
+ platformRecord: T,
+ typeSpecificAssertions: T.() -> Unit
+ ) {
+ assertThat(platformRecord.startTime).isEqualTo(START_TIME)
+ assertThat(platformRecord.startZoneOffset).isEqualTo(START_ZONE_OFFSET)
+ assertThat(platformRecord.endTime).isEqualTo(END_TIME)
+ assertThat(platformRecord.endZoneOffset).isEqualTo(END_ZONE_OFFSET)
+ assertThat(platformRecord.metadata).isEqualTo(PLATFORM_METADATA)
+ platformRecord.typeSpecificAssertions()
+ }
+
+ private fun <T : PlatformInstantRecord> assertPlatformRecord(platformRecord: T) =
+ assertPlatformRecord(platformRecord) {}
+
+ private fun <T : PlatformInstantRecord> assertPlatformRecord(
+ platformRecord: T,
+ typeSpecificAssertions: T.() -> Unit
+ ) {
+ assertThat(platformRecord.time).isEqualTo(TIME)
+ assertThat(platformRecord.zoneOffset).isEqualTo(ZONE_OFFSET)
+ assertThat(platformRecord.metadata).isEqualTo(PLATFORM_METADATA)
+ platformRecord.typeSpecificAssertions()
+ }
+
+ private fun <T : IntervalRecord> assertSdkRecord(sdkRecord: T) = assertSdkRecord(sdkRecord) {}
+
+ private fun <T : IntervalRecord> assertSdkRecord(
+ sdkRecord: T,
+ typeSpecificAssertions: T.() -> Unit
+ ) {
+ assertThat(sdkRecord.startTime).isEqualTo(START_TIME)
+ assertThat(sdkRecord.startZoneOffset).isEqualTo(START_ZONE_OFFSET)
+ assertThat(sdkRecord.endTime).isEqualTo(END_TIME)
+ assertThat(sdkRecord.endZoneOffset).isEqualTo(END_ZONE_OFFSET)
+ assertThat(sdkRecord.metadata.id).isEqualTo(METADATA.id)
+ assertThat(sdkRecord.metadata.dataOrigin).isEqualTo(METADATA.dataOrigin)
+ sdkRecord.typeSpecificAssertions()
+ }
+
+ private fun <T : InstantaneousRecord> assertSdkRecord(sdkRecord: T) =
+ assertSdkRecord(sdkRecord) {}
+
+ private fun <T : InstantaneousRecord> assertSdkRecord(
+ sdkRecord: T,
+ typeSpecificAssertions: T.() -> Unit
+ ) {
+ assertThat(sdkRecord.time).isEqualTo(TIME)
+ assertThat(sdkRecord.zoneOffset).isEqualTo(ZONE_OFFSET)
+ assertThat(sdkRecord.metadata.id).isEqualTo(METADATA.id)
+ assertThat(sdkRecord.metadata.dataOrigin).isEqualTo(METADATA.dataOrigin)
+ sdkRecord.typeSpecificAssertions()
+ }
+
+ private companion object {
+ val TIME: Instant = Instant.ofEpochMilli(1235L)
+ val ZONE_OFFSET: ZoneOffset = ZoneOffset.UTC
+
+ val START_TIME: Instant = Instant.ofEpochMilli(1234L)
+ val END_TIME: Instant = Instant.ofEpochMilli(56780L)
+ val START_ZONE_OFFSET: ZoneOffset = ZoneOffset.UTC
+ val END_ZONE_OFFSET: ZoneOffset = ZoneOffset.ofHours(2)
+
+ val METADATA = Metadata(id = "someId", dataOrigin = DataOrigin("somePackage"))
+
+ val PLATFORM_METADATA =
+ PlatformMetadataBuilder()
+ .setId("someId")
+ .setDataOrigin(PlatformDataOriginBuilder().setPackageName("somePackage").build())
+ .build()
+ }
+}
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RequestConvertersTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RequestConvertersTest.kt
new file mode 100644
index 0000000..05c40c0
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RequestConvertersTest.kt
@@ -0,0 +1,221 @@
+/*
+ * 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.health.connect.client.impl.platform.records
+
+import android.annotation.TargetApi
+import android.health.connect.LocalTimeRangeFilter
+import android.health.connect.TimeInstantRangeFilter
+import android.health.connect.datatypes.DataOrigin as PlatformDataOrigin
+import android.health.connect.datatypes.HeartRateRecord as PlatformHeartRateRecord
+import android.health.connect.datatypes.NutritionRecord as PlatformNutritionRecord
+import android.health.connect.datatypes.StepsRecord as PlatformStepsRecord
+import android.health.connect.datatypes.WheelchairPushesRecord as PlatformWheelchairPushesRecord
+import android.os.Build
+import androidx.health.connect.client.impl.platform.time.FakeTimeSource
+import androidx.health.connect.client.impl.platform.time.SystemDefaultTimeSource
+import androidx.health.connect.client.records.HeartRateRecord
+import androidx.health.connect.client.records.NutritionRecord
+import androidx.health.connect.client.records.StepsRecord
+import androidx.health.connect.client.records.WheelchairPushesRecord
+import androidx.health.connect.client.records.metadata.DataOrigin
+import androidx.health.connect.client.request.AggregateGroupByDurationRequest
+import androidx.health.connect.client.request.AggregateGroupByPeriodRequest
+import androidx.health.connect.client.request.AggregateRequest
+import androidx.health.connect.client.request.ChangesTokenRequest
+import androidx.health.connect.client.request.ReadRecordsRequest
+import androidx.health.connect.client.time.TimeRangeFilter
+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 java.time.Duration
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.Month
+import java.time.Period
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+@SmallTest
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+// Comment the SDK suppress to run on emulators lower than U.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+class RequestConvertersTest {
+
+ @Test
+ fun readRecordsRequest_fromSdkToPlatform() {
+ val sdkRequest =
+ ReadRecordsRequest(
+ StepsRecord::class,
+ TimeRangeFilter.between(Instant.ofEpochMilli(123L), Instant.ofEpochMilli(456L)),
+ setOf(DataOrigin("package1"), DataOrigin("package2"))
+ )
+
+ with(sdkRequest.toPlatformRequest(SystemDefaultTimeSource)) {
+ assertThat(recordType).isAssignableTo(PlatformStepsRecord::class.java)
+ assertThat(dataOrigins)
+ .containsExactly(
+ PlatformDataOrigin.Builder().setPackageName("package1").build(),
+ PlatformDataOrigin.Builder().setPackageName("package2").build()
+ )
+ }
+ }
+
+ @Test
+ fun timeRangeFilter_instant_fromSdkToPlatform() {
+ val sdkFilter =
+ TimeRangeFilter.between(Instant.ofEpochMilli(123L), Instant.ofEpochMilli(456L))
+
+ with(
+ sdkFilter.toPlatformTimeRangeFilter(SystemDefaultTimeSource) as TimeInstantRangeFilter
+ ) {
+ assertThat(endTime).isEqualTo(Instant.ofEpochMilli(456L))
+ }
+ }
+
+ @Test
+ fun timeRangeFilter_localDateTime_fromSdkToPlatform() {
+ val sdkFilter = TimeRangeFilter.before(LocalDateTime.of(2023, Month.MARCH, 10, 17, 30))
+
+ with(sdkFilter.toPlatformTimeRangeFilter(SystemDefaultTimeSource) as LocalTimeRangeFilter) {
+ assertThat(endTime).isEqualTo(LocalDateTime.of(2023, Month.MARCH, 10, 17, 30))
+ }
+ }
+
+ @Test
+ fun timeRangeFilter_fromSdkToPlatform_none() {
+
+ val sdkFilter = TimeRangeFilter.none()
+ val fakeTimeSource = FakeTimeSource()
+ fakeTimeSource.now = Instant.ofEpochMilli(123L)
+
+ with(sdkFilter.toPlatformTimeRangeFilter(fakeTimeSource) as TimeInstantRangeFilter) {
+ assertThat(startTime).isEqualTo(Instant.EPOCH)
+ assertThat(endTime).isEqualTo(fakeTimeSource.now)
+ }
+ }
+
+ @Test
+ fun changesTokenRequest_fromSdkToPlatform() {
+ val sdkRequest =
+ ChangesTokenRequest(
+ setOf(StepsRecord::class, HeartRateRecord::class),
+ setOf(DataOrigin("package1"), DataOrigin("package2"))
+ )
+
+ with(sdkRequest.toPlatformRequest()) {
+ assertThat(recordTypes)
+ .containsExactly(
+ PlatformStepsRecord::class.java,
+ PlatformHeartRateRecord::class.java
+ )
+ assertThat(dataOriginFilters)
+ .containsExactly(
+ PlatformDataOrigin.Builder().setPackageName("package1").build(),
+ PlatformDataOrigin.Builder().setPackageName("package2").build()
+ )
+ }
+ }
+
+ @Test
+ fun aggregateRequest_fromSdkToPlatform() {
+ val sdkRequest =
+ AggregateRequest(
+ setOf(StepsRecord.COUNT_TOTAL, NutritionRecord.CAFFEINE_TOTAL),
+ TimeRangeFilter.between(Instant.ofEpochMilli(123L), Instant.ofEpochMilli(456L)),
+ setOf(DataOrigin("package1"))
+ )
+
+ with(sdkRequest.toPlatformRequest(SystemDefaultTimeSource)) {
+ with(timeRangeFilter as TimeInstantRangeFilter) {
+ assertThat(startTime).isEqualTo(Instant.ofEpochMilli(123L))
+ assertThat(endTime).isEqualTo(Instant.ofEpochMilli(456L))
+ }
+ assertThat(aggregationTypes)
+ .containsExactly(
+ PlatformStepsRecord.STEPS_COUNT_TOTAL,
+ PlatformNutritionRecord.CAFFEINE_TOTAL
+ )
+ assertThat(dataOriginsFilters)
+ .containsExactly(PlatformDataOrigin.Builder().setPackageName("package1").build())
+ }
+ }
+
+ @Test
+ fun aggregateGroupByDurationRequest_fromSdkToPlatform() {
+ val sdkRequest =
+ AggregateGroupByDurationRequest(
+ setOf(NutritionRecord.ENERGY_TOTAL),
+ TimeRangeFilter.between(Instant.ofEpochMilli(123L), Instant.ofEpochMilli(456L)),
+ Duration.ofDays(1),
+ setOf(DataOrigin("package1"), DataOrigin("package2"))
+ )
+
+ with(sdkRequest.toPlatformRequest(SystemDefaultTimeSource)) {
+ with(timeRangeFilter as TimeInstantRangeFilter) {
+ assertThat(startTime).isEqualTo(Instant.ofEpochMilli(123L))
+ assertThat(endTime).isEqualTo(Instant.ofEpochMilli(456L))
+ }
+ assertThat(aggregationTypes).containsExactly(PlatformNutritionRecord.ENERGY_TOTAL)
+ assertThat(dataOriginsFilters)
+ .containsExactly(
+ PlatformDataOrigin.Builder().setPackageName("package1").build(),
+ PlatformDataOrigin.Builder().setPackageName("package2").build()
+ )
+ }
+ }
+
+ @Test
+ fun aggregateGroupByPeriodRequest_fromSdkToPlatform() {
+ val sdkRequest =
+ AggregateGroupByPeriodRequest(
+ setOf(HeartRateRecord.BPM_MAX, HeartRateRecord.BPM_MIN, HeartRateRecord.BPM_AVG),
+ TimeRangeFilter.between(Instant.ofEpochMilli(123L), Instant.ofEpochMilli(456L)),
+ Period.ofDays(1),
+ setOf(DataOrigin("package1"), DataOrigin("package2"), DataOrigin("package3"))
+ )
+
+ with(sdkRequest.toPlatformRequest(SystemDefaultTimeSource)) {
+ with(timeRangeFilter as TimeInstantRangeFilter) {
+ assertThat(startTime).isEqualTo(Instant.ofEpochMilli(123L))
+ assertThat(endTime).isEqualTo(Instant.ofEpochMilli(456L))
+ }
+ assertThat(aggregationTypes)
+ .containsExactly(
+ PlatformHeartRateRecord.BPM_MAX,
+ PlatformHeartRateRecord.BPM_MIN,
+ PlatformHeartRateRecord.BPM_AVG
+ )
+ assertThat(dataOriginsFilters)
+ .containsExactly(
+ PlatformDataOrigin.Builder().setPackageName("package1").build(),
+ PlatformDataOrigin.Builder().setPackageName("package2").build(),
+ PlatformDataOrigin.Builder().setPackageName("package3").build()
+ )
+ }
+ }
+
+ @Test
+ fun toAggregationType_convertFromSdkToPlatform() {
+ assertThat(WheelchairPushesRecord.COUNT_TOTAL.toAggregationType())
+ .isEqualTo(PlatformWheelchairPushesRecord.WHEEL_CHAIR_PUSHES_COUNT_TOTAL)
+ assertThat(NutritionRecord.ENERGY_TOTAL.toAggregationType())
+ .isEqualTo(PlatformNutritionRecord.ENERGY_TOTAL)
+ }
+}
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/ResponseConvertersTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/ResponseConvertersTest.kt
new file mode 100644
index 0000000..986a99b
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/ResponseConvertersTest.kt
@@ -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.health.connect.client.impl.platform.records
+
+import android.annotation.TargetApi
+import android.health.connect.datatypes.units.Energy as PlatformEnergy
+import android.health.connect.datatypes.units.Length as PlatformLength
+import android.health.connect.datatypes.units.Mass as PlatformMass
+import android.health.connect.datatypes.units.Power as PlatformPower
+import android.health.connect.datatypes.units.Volume as PlatformVolume
+import android.os.Build
+import androidx.health.connect.client.aggregate.AggregateMetric
+import androidx.health.connect.client.records.BasalMetabolicRateRecord
+import androidx.health.connect.client.records.DistanceRecord
+import androidx.health.connect.client.records.ExerciseSessionRecord
+import androidx.health.connect.client.records.FloorsClimbedRecord
+import androidx.health.connect.client.records.HeartRateRecord
+import androidx.health.connect.client.records.HydrationRecord
+import androidx.health.connect.client.records.NutritionRecord
+import androidx.health.connect.client.records.PowerRecord
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Correspondence
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+@SmallTest
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+// Comment the SDK suppress to run on emulators lower than U.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+class ResponseConvertersTest {
+
+ private val tolerance = Correspondence.tolerance(1e-6)
+
+ @Test
+ fun getLongMetricValues_convertsValueAccurately() {
+ val metricValues =
+ getLongMetricValues(
+ mapOf(
+ HeartRateRecord.BPM_MIN as AggregateMetric<Any> to 53L,
+ ExerciseSessionRecord.EXERCISE_DURATION_TOTAL as AggregateMetric<Any> to 60_000L
+ )
+ )
+ assertThat(metricValues)
+ .containsExactly(
+ HeartRateRecord.BPM_MIN.metricKey,
+ 53L,
+ ExerciseSessionRecord.EXERCISE_DURATION_TOTAL.metricKey,
+ 60_000L
+ )
+ }
+
+ @Test
+ fun getLongMetricValues_ignoresNonLongMetricTypes() {
+ val metricValues =
+ getLongMetricValues(
+ mapOf(
+ NutritionRecord.ENERGY_TOTAL as AggregateMetric<Any> to
+ PlatformEnergy.fromCalories(418_400.0)
+ )
+ )
+ assertThat(metricValues).isEmpty()
+ }
+
+ @Test
+ fun getDoubleMetricValues_convertsEnergyToKilocalories() {
+ val metricValues =
+ getDoubleMetricValues(
+ mapOf(
+ NutritionRecord.ENERGY_TOTAL as AggregateMetric<Any> to
+ PlatformEnergy.fromCalories(418_400.0)
+ )
+ )
+ assertThat(metricValues)
+ .comparingValuesUsing(tolerance)
+ .containsExactly(NutritionRecord.ENERGY_TOTAL.metricKey, 418.4)
+ }
+
+ @Test
+ fun getDoubleMetricValues_convertsLengthToMeters() {
+ val metricValues =
+ getDoubleMetricValues(
+ mapOf(
+ DistanceRecord.DISTANCE_TOTAL as AggregateMetric<Any> to
+ PlatformLength.fromMeters(50.0)
+ )
+ )
+ assertThat(metricValues).containsExactly(DistanceRecord.DISTANCE_TOTAL.metricKey, 50.0)
+ }
+
+ @Test
+ fun getDoubleMetricValues_convertsMassToGrams() {
+ val metricValues =
+ getDoubleMetricValues(
+ mapOf(
+ NutritionRecord.BIOTIN_TOTAL as AggregateMetric<Any> to
+ PlatformMass.fromGrams(88.0)
+ )
+ )
+ assertThat(metricValues).containsExactly(NutritionRecord.BIOTIN_TOTAL.metricKey, 88.0)
+ }
+
+ @Test
+ fun getDoubleMetricValues_convertsPowerToWatts() {
+ val metricValues =
+ getDoubleMetricValues(
+ mapOf(
+ PowerRecord.POWER_AVG as AggregateMetric<Any> to PlatformPower.fromWatts(366.0)
+ )
+ )
+ assertThat(metricValues).containsExactly(PowerRecord.POWER_AVG.metricKey, 366.0)
+ }
+
+ @Test
+ fun getDoubleMetricValues_convertsVolumeToLiters() {
+ val metricValues =
+ getDoubleMetricValues(
+ mapOf(
+ HydrationRecord.VOLUME_TOTAL as AggregateMetric<Any> to
+ PlatformVolume.fromLiters(1.5)
+ )
+ )
+ assertThat(metricValues).containsExactly(HydrationRecord.VOLUME_TOTAL.metricKey, 1.5)
+ }
+
+ @Test
+ fun getDoubleMetricValues_ignoresNonDoubleMetricTypes() {
+ val metricValues =
+ getDoubleMetricValues(mapOf(HeartRateRecord.BPM_MIN as AggregateMetric<Any> to 53L))
+ assertThat(metricValues).isEmpty()
+ }
+
+ @Test
+ fun getLongMetricValues_handlesMultipleMetrics() {
+ val metricValues =
+ getLongMetricValues(
+ mapOf(
+ HeartRateRecord.BPM_MIN as AggregateMetric<Any> to 53L,
+ ExerciseSessionRecord.EXERCISE_DURATION_TOTAL as AggregateMetric<Any> to
+ 60_000L,
+ NutritionRecord.ENERGY_TOTAL as AggregateMetric<Any> to
+ PlatformEnergy.fromCalories(418_400.0),
+ DistanceRecord.DISTANCE_TOTAL as AggregateMetric<Any> to
+ PlatformLength.fromMeters(50.0),
+ NutritionRecord.BIOTIN_TOTAL as AggregateMetric<Any> to
+ PlatformMass.fromGrams(88.0),
+ PowerRecord.POWER_AVG as AggregateMetric<Any> to PlatformPower.fromWatts(366.0),
+ HydrationRecord.VOLUME_TOTAL as AggregateMetric<Any> to
+ PlatformVolume.fromLiters(1.5),
+ FloorsClimbedRecord.FLOORS_CLIMBED_TOTAL as AggregateMetric<Any> to 10L,
+ BasalMetabolicRateRecord.BASAL_CALORIES_TOTAL as AggregateMetric<Any> to
+ PlatformPower.fromWatts(500.0),
+ )
+ )
+ assertThat(metricValues)
+ .containsExactly(
+ HeartRateRecord.BPM_MIN.metricKey,
+ 53L,
+ ExerciseSessionRecord.EXERCISE_DURATION_TOTAL.metricKey,
+ 60_000L
+ )
+ }
+
+ @Test
+ fun getDoubleMetricValues_handlesMultipleMetrics() {
+ val metricValues =
+ getDoubleMetricValues(
+ mapOf(
+ HeartRateRecord.BPM_MIN as AggregateMetric<Any> to 53L,
+ ExerciseSessionRecord.EXERCISE_DURATION_TOTAL as AggregateMetric<Any> to
+ 60_000L,
+ NutritionRecord.ENERGY_TOTAL as AggregateMetric<Any> to
+ PlatformEnergy.fromCalories(418_400.0),
+ DistanceRecord.DISTANCE_TOTAL as AggregateMetric<Any> to
+ PlatformLength.fromMeters(50.0),
+ NutritionRecord.BIOTIN_TOTAL as AggregateMetric<Any> to
+ PlatformMass.fromGrams(88.0),
+ PowerRecord.POWER_AVG as AggregateMetric<Any> to PlatformPower.fromWatts(366.0),
+ HydrationRecord.VOLUME_TOTAL as AggregateMetric<Any> to
+ PlatformVolume.fromLiters(1.5),
+ FloorsClimbedRecord.FLOORS_CLIMBED_TOTAL as AggregateMetric<Any> to 10.0,
+ BasalMetabolicRateRecord.BASAL_CALORIES_TOTAL as AggregateMetric<Any> to
+ PlatformEnergy.fromCalories(836_800.0),
+ )
+ )
+ assertThat(metricValues)
+ .comparingValuesUsing(tolerance)
+ .containsExactly(
+ NutritionRecord.ENERGY_TOTAL.metricKey,
+ 418.4,
+ DistanceRecord.DISTANCE_TOTAL.metricKey,
+ 50.0,
+ NutritionRecord.BIOTIN_TOTAL.metricKey,
+ 88,
+ PowerRecord.POWER_AVG.metricKey,
+ 366.0,
+ HydrationRecord.VOLUME_TOTAL.metricKey,
+ 1.5,
+ FloorsClimbedRecord.FLOORS_CLIMBED_TOTAL.metricKey,
+ 10.0,
+ BasalMetabolicRateRecord.BASAL_CALORIES_TOTAL.metricKey,
+ 836.8
+ )
+ }
+}
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/time/FakeTimeSource.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/time/FakeTimeSource.kt
new file mode 100644
index 0000000..270eaf4
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/time/FakeTimeSource.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.health.connect.client.impl.platform.time
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import java.time.Instant
+
+@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class FakeTimeSource : TimeSource {
+ override lateinit var now: Instant
+}
\ No newline at end of file
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
index 96f9b86..d49ba6a 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
@@ -25,11 +25,14 @@
import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
import androidx.core.content.pm.PackageInfoCompat
+import androidx.core.os.BuildCompat
+import androidx.core.os.BuildCompat.PrereleaseSdkCheck
import androidx.health.connect.client.aggregate.AggregateMetric
import androidx.health.connect.client.aggregate.AggregationResult
import androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration
import androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod
import androidx.health.connect.client.impl.HealthConnectClientImpl
+import androidx.health.connect.client.impl.HealthConnectClientUpsideDownImpl
import androidx.health.connect.client.records.Record
import androidx.health.connect.client.records.metadata.DataOrigin
import androidx.health.connect.client.request.AggregateGroupByDurationRequest
@@ -314,13 +317,23 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal const val DEFAULT_PROVIDER_MIN_VERSION_CODE = 35000
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ const val ACTION_HEALTH_CONNECT_SETTINGS_LEGACY =
+ "androidx.health.ACTION_HEALTH_CONNECT_SETTINGS"
+
/**
* Intent action to open Health Connect settings on this phone. Developers should use this
* if they want to re-direct the user to Health Connect.
*/
+ @get:PrereleaseSdkCheck
+ @get:Suppress("IllegalExperimentalApiUsage")
@get:JvmName("getHealthConnectSettingsAction")
@JvmStatic
- val ACTION_HEALTH_CONNECT_SETTINGS = "androidx.health.ACTION_HEALTH_CONNECT_SETTINGS"
+ @PrereleaseSdkCheck
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET", "IllegalExperimentalApiUsage")
+ val ACTION_HEALTH_CONNECT_SETTINGS =
+ if (BuildCompat.isAtLeastU()) "android.health.connect.action.HEALTH_HOME_SETTINGS"
+ else "androidx.health.ACTION_HEALTH_CONNECT_SETTINGS"
/**
* The Health Connect SDK is not unavailable on this device at the time. This can be due to
@@ -368,6 +381,8 @@
@JvmOverloads
@JvmStatic
@AvailabilityStatus
+ @PrereleaseSdkCheck
+ @Suppress("IllegalExperimentalApiUsage")
fun getSdkStatus(
context: Context,
providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME,
@@ -383,6 +398,25 @@
return SDK_AVAILABLE
}
+ @JvmOverloads
+ @JvmStatic
+ @AvailabilityStatus
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun getSdkStatusLegacy(
+ context: Context,
+ providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME,
+ ): Int {
+ @Suppress("Deprecation")
+ if (!isApiSupported()) {
+ return SDK_UNAVAILABLE
+ }
+ @Suppress("Deprecation")
+ if (!isProviderAvailableLegacy(context, providerPackageName)) {
+ return SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED
+ }
+ return SDK_AVAILABLE
+ }
+
/**
* Determines whether the current Health Connect SDK is supported on this device. If it is
* not supported, then installing any provider will not help - instead disable the
@@ -408,10 +442,15 @@
@JvmOverloads
@JvmStatic
@Deprecated("use sdkStatus()", ReplaceWith("sdkStatus(context)"))
+ @PrereleaseSdkCheck
+ @Suppress("IllegalExperimentalApiUsage")
public fun isProviderAvailable(
context: Context,
providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME,
): Boolean {
+ if (BuildCompat.isAtLeastU()) {
+ return true
+ }
@Suppress("Deprecation")
if (!isApiSupported()) {
return false
@@ -433,6 +472,8 @@
*/
@JvmOverloads
@JvmStatic
+ @PrereleaseSdkCheck
+ @Suppress("IllegalExperimentalApiUsage")
public fun getOrCreate(
context: Context,
providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME,
@@ -445,6 +486,45 @@
if (!isProviderAvailable(context, providerPackageName)) {
throw IllegalStateException("Service not available")
}
+
+ if (BuildCompat.isAtLeastU()) {
+ return HealthConnectClientUpsideDownImpl(context)
+ }
+ return HealthConnectClientImpl(
+ HealthDataService.getClient(context, providerPackageName)
+ )
+ }
+
+ @JvmOverloads
+ @JvmStatic
+ @Deprecated("use getSdkStatusLegacy()")
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun isProviderAvailableLegacy(
+ context: Context,
+ providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME,
+ ): Boolean {
+ @Suppress("Deprecation")
+ if (!isApiSupported()) {
+ return false
+ }
+ return isPackageInstalled(context.packageManager, providerPackageName)
+ }
+
+ @JvmOverloads
+ @JvmStatic
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun getOrCreateLegacy(
+ context: Context,
+ providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME,
+ ): HealthConnectClient {
+ @Suppress("Deprecation")
+ if (!isApiSupported()) {
+ throw UnsupportedOperationException("SDK version too low")
+ }
+ @Suppress("Deprecation")
+ if (!isProviderAvailableLegacy(context, providerPackageName)) {
+ throw IllegalStateException("Service not available")
+ }
return HealthConnectClientImpl(
HealthDataService.getClient(context, providerPackageName)
)
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
index 1be95806..d183e7c 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
@@ -16,9 +16,13 @@
package androidx.health.connect.client
import androidx.activity.result.contract.ActivityResultContract
+import androidx.annotation.RestrictTo
+import androidx.core.os.BuildCompat
+import androidx.core.os.BuildCompat.PrereleaseSdkCheck
import androidx.health.connect.client.HealthConnectClient.Companion.DEFAULT_PROVIDER_PACKAGE_NAME
import androidx.health.connect.client.permission.HealthDataRequestPermissionsInternal
import androidx.health.connect.client.permission.HealthPermission
+import androidx.health.connect.client.permission.platform.HealthDataRequestPermissionsUpsideDownCake
@JvmDefaultWithCompatibility
/** Interface for operations related to permissions. */
@@ -45,6 +49,17 @@
suspend fun revokeAllPermissions()
companion object {
+
+ @JvmStatic
+ @JvmOverloads
+ @Suppress("IllegalExperimentalApiUsage")
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ fun createRequestPermissionResultContractLegacy(
+ providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME
+ ): ActivityResultContract<Set<String>, Set<String>> {
+ return HealthDataRequestPermissionsInternal(providerPackageName = providerPackageName)
+ }
+
/**
* Creates an [ActivityResultContract] to request Health permissions.
*
@@ -55,9 +70,14 @@
*/
@JvmStatic
@JvmOverloads
+ @PrereleaseSdkCheck
+ @Suppress("IllegalExperimentalApiUsage")
fun createRequestPermissionResultContract(
providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME
): ActivityResultContract<Set<String>, Set<String>> {
+ if (BuildCompat.isAtLeastU()) {
+ return HealthDataRequestPermissionsUpsideDownCake()
+ }
return HealthDataRequestPermissionsInternal(providerPackageName = providerPackageName)
}
}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt
new file mode 100644
index 0000000..2086d04
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt
@@ -0,0 +1,334 @@
+/*
+ * 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.health.connect.client.impl
+
+import android.content.Context
+import android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED
+import android.content.pm.PackageManager.GET_PERMISSIONS
+import android.content.pm.PackageManager.PackageInfoFlags
+import android.health.connect.HealthConnectException
+import android.health.connect.HealthConnectManager
+import android.health.connect.ReadRecordsRequestUsingIds
+import android.health.connect.RecordIdFilter
+import android.health.connect.changelog.ChangeLogsRequest
+import android.os.RemoteException
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.health.connect.client.HealthConnectClient
+import androidx.health.connect.client.PermissionController
+import androidx.health.connect.client.aggregate.AggregationResult
+import androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration
+import androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod
+import androidx.health.connect.client.changes.DeletionChange
+import androidx.health.connect.client.changes.UpsertionChange
+import androidx.health.connect.client.impl.platform.asOutcomeReceiver
+import androidx.health.connect.client.impl.platform.records.toPlatformRecord
+import androidx.health.connect.client.impl.platform.records.toPlatformRecordClass
+import androidx.health.connect.client.impl.platform.records.toPlatformRequest
+import androidx.health.connect.client.impl.platform.records.toPlatformTimeRangeFilter
+import androidx.health.connect.client.impl.platform.records.toSdkRecord
+import androidx.health.connect.client.impl.platform.records.toSdkResponse
+import androidx.health.connect.client.impl.platform.response.toKtResponse
+import androidx.health.connect.client.impl.platform.time.SystemDefaultTimeSource
+import androidx.health.connect.client.impl.platform.time.TimeSource
+import androidx.health.connect.client.impl.platform.toKtException
+import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_PREFIX
+import androidx.health.connect.client.records.Record
+import androidx.health.connect.client.request.AggregateGroupByDurationRequest
+import androidx.health.connect.client.request.AggregateGroupByPeriodRequest
+import androidx.health.connect.client.request.AggregateRequest
+import androidx.health.connect.client.request.ChangesTokenRequest
+import androidx.health.connect.client.request.ReadRecordsRequest
+import androidx.health.connect.client.response.ChangesResponse
+import androidx.health.connect.client.response.InsertRecordsResponse
+import androidx.health.connect.client.response.ReadRecordResponse
+import androidx.health.connect.client.response.ReadRecordsResponse
+import androidx.health.connect.client.time.TimeRangeFilter
+import kotlin.reflect.KClass
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * Implements the [HealthConnectClient] with APIs in UpsideDownCake.
+ *
+ * @suppress
+ */
+@RequiresApi(api = 34)
+class HealthConnectClientUpsideDownImpl : HealthConnectClient, PermissionController {
+
+ private val executor = Dispatchers.Default.asExecutor()
+
+ private val context: Context
+ private val timeSource: TimeSource
+ private val healthConnectManager: HealthConnectManager
+ private val revokePermissionsFunction: (Collection<String>) -> Unit
+
+ constructor(
+ context: Context
+ ) : this(context, SystemDefaultTimeSource, context::revokeSelfPermissionsOnKill)
+
+ @VisibleForTesting
+ internal constructor(
+ context: Context,
+ timeSource: TimeSource,
+ revokePermissionsFunction: (Collection<String>) -> Unit
+ ) {
+ this.context = context
+ this.timeSource = timeSource
+ this.healthConnectManager =
+ context.getSystemService(Context.HEALTHCONNECT_SERVICE) as HealthConnectManager
+ this.revokePermissionsFunction = revokePermissionsFunction
+ }
+
+ override val permissionController: PermissionController
+ get() = this
+
+ override suspend fun insertRecords(records: List<Record>): InsertRecordsResponse {
+ val response = wrapPlatformException {
+ suspendCancellableCoroutine { continuation ->
+ healthConnectManager.insertRecords(
+ records.map { it.toPlatformRecord() },
+ executor,
+ continuation.asOutcomeReceiver()
+ )
+ }
+ }
+ return response.toKtResponse()
+ }
+
+ override suspend fun updateRecords(records: List<Record>) {
+ wrapPlatformException {
+ suspendCancellableCoroutine { continuation ->
+ healthConnectManager.updateRecords(
+ records.map { it.toPlatformRecord() },
+ executor,
+ continuation.asOutcomeReceiver()
+ )
+ }
+ }
+ }
+
+ override suspend fun deleteRecords(
+ recordType: KClass<out Record>,
+ recordIdsList: List<String>,
+ clientRecordIdsList: List<String>
+ ) {
+ wrapPlatformException {
+ suspendCancellableCoroutine { continuation ->
+ healthConnectManager.deleteRecords(
+ buildList {
+ recordIdsList.forEach {
+ add(RecordIdFilter.fromId(recordType.toPlatformRecordClass(), it))
+ }
+ clientRecordIdsList.forEach {
+ add(
+ RecordIdFilter.fromClientRecordId(
+ recordType.toPlatformRecordClass(),
+ it
+ )
+ )
+ }
+ },
+ executor,
+ continuation.asOutcomeReceiver()
+ )
+ }
+ }
+ }
+
+ override suspend fun deleteRecords(
+ recordType: KClass<out Record>,
+ timeRangeFilter: TimeRangeFilter
+ ) {
+ wrapPlatformException {
+ suspendCancellableCoroutine { continuation ->
+ healthConnectManager.deleteRecords(
+ recordType.toPlatformRecordClass(),
+ timeRangeFilter.toPlatformTimeRangeFilter(timeSource),
+ executor,
+ continuation.asOutcomeReceiver()
+ )
+ }
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST") // Safe to cast as the type should match
+ override suspend fun <T : Record> readRecord(
+ recordType: KClass<T>,
+ recordId: String
+ ): ReadRecordResponse<T> {
+ val response = wrapPlatformException {
+ suspendCancellableCoroutine { continuation ->
+ healthConnectManager.readRecords(
+ ReadRecordsRequestUsingIds.Builder(recordType.toPlatformRecordClass())
+ .addId(recordId)
+ .build(),
+ executor,
+ continuation.asOutcomeReceiver()
+ )
+ }
+ }
+ if (response.records.isEmpty()) {
+ throw RemoteException("No records")
+ }
+ return ReadRecordResponse(response.records[0].toSdkRecord() as T)
+ }
+
+ @Suppress("UNCHECKED_CAST") // Safe to cast as the type should match
+ override suspend fun <T : Record> readRecords(
+ request: ReadRecordsRequest<T>
+ ): ReadRecordsResponse<T> {
+ val response = wrapPlatformException {
+ suspendCancellableCoroutine { continuation ->
+ healthConnectManager.readRecords(
+ request.toPlatformRequest(timeSource),
+ executor,
+ continuation.asOutcomeReceiver()
+ )
+ }
+ }
+ return ReadRecordsResponse(
+ response.records.map { it.toSdkRecord() as T },
+ pageToken = response.nextPageToken.takeUnless { it == -1L }?.toString()
+ )
+ }
+
+ override suspend fun aggregate(request: AggregateRequest): AggregationResult {
+ return wrapPlatformException {
+ suspendCancellableCoroutine { continuation ->
+ healthConnectManager.aggregate(
+ request.toPlatformRequest(timeSource),
+ executor,
+ continuation.asOutcomeReceiver()
+ )
+ }
+ }
+ .toSdkResponse(request.metrics)
+ }
+
+ override suspend fun aggregateGroupByDuration(
+ request: AggregateGroupByDurationRequest
+ ): List<AggregationResultGroupedByDuration> {
+ return wrapPlatformException {
+ suspendCancellableCoroutine { continuation ->
+ healthConnectManager.aggregateGroupByDuration(
+ request.toPlatformRequest(timeSource),
+ request.timeRangeSlicer,
+ executor,
+ continuation.asOutcomeReceiver()
+ )
+ }
+ }
+ .map { it.toSdkResponse(request.metrics) }
+ }
+
+ override suspend fun aggregateGroupByPeriod(
+ request: AggregateGroupByPeriodRequest
+ ): List<AggregationResultGroupedByPeriod> {
+ return wrapPlatformException {
+ suspendCancellableCoroutine { continuation ->
+ healthConnectManager.aggregateGroupByPeriod(
+ request.toPlatformRequest(timeSource),
+ request.timeRangeSlicer,
+ executor,
+ continuation.asOutcomeReceiver()
+ )
+ }
+ }
+ .map { it.toSdkResponse(request.metrics) }
+ }
+
+ override suspend fun getChangesToken(request: ChangesTokenRequest): String {
+ return wrapPlatformException {
+ suspendCancellableCoroutine { continuation ->
+ healthConnectManager.getChangeLogToken(
+ request.toPlatformRequest(),
+ executor,
+ continuation.asOutcomeReceiver()
+ )
+ }
+ }
+ .token
+ }
+
+ override suspend fun getChanges(changesToken: String): ChangesResponse {
+ try {
+ val response = suspendCancellableCoroutine { continuation ->
+ healthConnectManager.getChangeLogs(
+ ChangeLogsRequest.Builder(changesToken).build(),
+ executor,
+ continuation.asOutcomeReceiver()
+ )
+ }
+ return ChangesResponse(
+ buildList {
+ response.upsertedRecords.forEach { add(UpsertionChange(it.toSdkRecord())) }
+ response.deletedLogs.forEach { add(DeletionChange(it.deletedRecordId)) }
+ },
+ response.nextChangesToken,
+ response.hasMorePages(),
+ changesTokenExpired = false
+ )
+ } catch (e: HealthConnectException) {
+ // Handle invalid token
+ if (e.errorCode == HealthConnectException.ERROR_INVALID_ARGUMENT) {
+ return ChangesResponse(
+ changes = listOf(),
+ nextChangesToken = "",
+ hasMore = false,
+ changesTokenExpired = true
+ )
+ }
+ throw e.toKtException()
+ }
+ }
+
+ override suspend fun getGrantedPermissions(): Set<String> {
+ context.packageManager
+ .getPackageInfo(context.packageName, PackageInfoFlags.of(GET_PERMISSIONS.toLong()))
+ .let {
+ return buildSet {
+ for (i in it.requestedPermissions.indices) {
+ if (
+ it.requestedPermissions[i].startsWith(PERMISSION_PREFIX) &&
+ it.requestedPermissionsFlags[i] and REQUESTED_PERMISSION_GRANTED > 0
+ ) {
+ add(it.requestedPermissions[i])
+ }
+ }
+ }
+ }
+ }
+
+ override suspend fun revokeAllPermissions() {
+ val allHealthPermissions =
+ context.packageManager
+ .getPackageInfo(context.packageName, PackageInfoFlags.of(GET_PERMISSIONS.toLong()))
+ .requestedPermissions
+ .filter { it.startsWith(PERMISSION_PREFIX) }
+ revokePermissionsFunction(allHealthPermissions)
+ }
+
+ private suspend fun <T> wrapPlatformException(function: suspend () -> T): T {
+ return try {
+ function()
+ } catch (e: HealthConnectException) {
+ throw e.toKtException()
+ }
+ }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/ContinuationExtensions.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/ContinuationExtensions.kt
new file mode 100644
index 0000000..5135043
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/ContinuationExtensions.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.
+ */
+
+// TODO(b/269468056): Remove this file and use androidx.core.os implementation
+
+@file:RestrictTo(RestrictTo.Scope.LIBRARY)
+@file:RequiresApi(api = 34)
+
+package androidx.health.connect.client.impl.platform
+
+import android.os.OutcomeReceiver
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+
+internal fun <R, E : Throwable> Continuation<R>.asOutcomeReceiver(): OutcomeReceiver<R, E> =
+ ContinuationOutcomeReceiver(this)
+
+private class ContinuationOutcomeReceiver<R, E : Throwable>(
+ private val continuation: Continuation<R>
+) : OutcomeReceiver<R, E>, AtomicBoolean(false) {
+ @Suppress("WRONG_NULLABILITY_FOR_JAVA_OVERRIDE")
+ override fun onResult(result: R) {
+ // Do not attempt to resume more than once, even if the caller of the returned
+ // OutcomeReceiver is buggy and tries anyway.
+ if (compareAndSet(false, true)) {
+ continuation.resume(result)
+ }
+ }
+
+ override fun onError(error: E) {
+ // Do not attempt to resume more than once, even if the caller of the returned
+ // OutcomeReceiver is buggy and tries anyway.
+ if (compareAndSet(false, true)) {
+ continuation.resumeWithException(error)
+ }
+ }
+
+ override fun toString() = "ContinuationOutcomeReceiver(outcomeReceived = ${get()})"
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/ExceptionConverter.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/ExceptionConverter.kt
new file mode 100644
index 0000000..be40e96
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/ExceptionConverter.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:RequiresApi(api = 34)
+
+package androidx.health.connect.client.impl.platform
+
+import android.health.connect.HealthConnectException
+import android.os.RemoteException
+import androidx.annotation.RequiresApi
+import java.io.IOException
+import java.lang.IllegalArgumentException
+import java.lang.IllegalStateException
+
+/** Converts exception returned by the platform to one of standard exception class hierarchy. */
+internal fun HealthConnectException.toKtException(): Exception {
+ return when (errorCode) {
+ HealthConnectException.ERROR_IO -> IOException(message)
+ HealthConnectException.ERROR_REMOTE -> RemoteException(message)
+ HealthConnectException.ERROR_SECURITY -> SecurityException(message)
+ HealthConnectException.ERROR_INVALID_ARGUMENT -> IllegalArgumentException(message)
+ else -> IllegalStateException(message)
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/package-info.java
similarity index 62%
rename from appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java
rename to health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/package-info.java
index c01917e..dc32f56 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/package-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Android Open Source Project
+ * Copyright (C) 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.
@@ -14,15 +14,12 @@
* limitations under the License.
*/
-package androidx.appsearch.observer;
-
-import androidx.annotation.RestrictTo;
-
/**
- * @deprecated use {@link ObserverCallback} instead.
+ * Helps with conversions to the platform record and API objects.
+ *
* @hide
*/
-// TODO(b/209734214): Remove this after dogfooders and devices have migrated away from this class.
-@Deprecated
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public interface AppSearchObserverCallback extends ObserverCallback {}
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.health.connect.client.impl.platform;
+
+import androidx.annotation.RestrictTo;
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/AggregationMappings.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/AggregationMappings.kt
new file mode 100644
index 0000000..1d414a5
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/AggregationMappings.kt
@@ -0,0 +1,184 @@
+/*
+ * 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:RestrictTo(RestrictTo.Scope.LIBRARY)
+@file:RequiresApi(api = 34)
+
+package androidx.health.connect.client.impl.platform.records
+
+import android.health.connect.datatypes.ActiveCaloriesBurnedRecord as PlatformActiveCaloriesBurnedRecord
+import android.health.connect.datatypes.AggregationType as PlatformAggregateMetric
+import android.health.connect.datatypes.BasalMetabolicRateRecord as PlatformBasalMetabolicRateRecord
+import android.health.connect.datatypes.DistanceRecord as PlatformDistanceRecord
+import android.health.connect.datatypes.ElevationGainedRecord as PlatformElevationGainedRecord
+import android.health.connect.datatypes.FloorsClimbedRecord as PlatformFloorsClimbedRecord
+import android.health.connect.datatypes.HeartRateRecord as PlatformHeartRateRecord
+import android.health.connect.datatypes.HeightRecord as PlatformHeightRecord
+import android.health.connect.datatypes.HydrationRecord as PlatformHydrationRecord
+import android.health.connect.datatypes.NutritionRecord as PlatformNutritionRecord
+import android.health.connect.datatypes.PowerRecord as PlatformPowerRecord
+import android.health.connect.datatypes.StepsRecord as PlatformStepsRecord
+import android.health.connect.datatypes.TotalCaloriesBurnedRecord as PlatformTotalCaloriesBurnedRecord
+import android.health.connect.datatypes.WeightRecord as PlatformWeightRecord
+import android.health.connect.datatypes.WheelchairPushesRecord as PlatformWheelchairPushesRecord
+import android.health.connect.datatypes.units.Energy as PlatformEnergy
+import android.health.connect.datatypes.units.Length as PlatformLength
+import android.health.connect.datatypes.units.Mass as PlatformMass
+import android.health.connect.datatypes.units.Power as PlatformPower
+import android.health.connect.datatypes.units.Volume as PlatformVolume
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.aggregate.AggregateMetric
+import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
+import androidx.health.connect.client.records.BasalMetabolicRateRecord
+import androidx.health.connect.client.records.DistanceRecord
+import androidx.health.connect.client.records.ElevationGainedRecord
+import androidx.health.connect.client.records.ExerciseSessionRecord
+import androidx.health.connect.client.records.FloorsClimbedRecord
+import androidx.health.connect.client.records.HeartRateRecord
+import androidx.health.connect.client.records.HeightRecord
+import androidx.health.connect.client.records.HydrationRecord
+import androidx.health.connect.client.records.NutritionRecord
+import androidx.health.connect.client.records.PowerRecord
+import androidx.health.connect.client.records.RestingHeartRateRecord
+import androidx.health.connect.client.records.SleepSessionRecord
+import androidx.health.connect.client.records.StepsRecord
+import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
+import androidx.health.connect.client.records.WeightRecord
+import androidx.health.connect.client.records.WheelchairPushesRecord
+import androidx.health.connect.client.units.Energy
+import androidx.health.connect.client.units.Length
+import androidx.health.connect.client.units.Mass
+import androidx.health.connect.client.units.Power
+import androidx.health.connect.client.units.Volume
+import java.time.Duration
+
+internal val DOUBLE_AGGREGATION_METRIC_TYPE_MAP:
+ Map<AggregateMetric<Double>, PlatformAggregateMetric<Double>> =
+ mapOf(
+ FloorsClimbedRecord.FLOORS_CLIMBED_TOTAL to
+ PlatformFloorsClimbedRecord.FLOORS_CLIMBED_TOTAL,
+ )
+
+internal val DURATION_AGGREGATION_METRIC_TYPE_MAP:
+ Map<AggregateMetric<Duration>, PlatformAggregateMetric<Long>> =
+ mapOf(
+ ExerciseSessionRecord.EXERCISE_DURATION_TOTAL to
+ PlatformExerciseSessionRecord.EXERCISE_DURATION_TOTAL,
+ SleepSessionRecord.SLEEP_DURATION_TOTAL to PlatformSleepSessionRecord.SLEEP_DURATION_TOTAL
+ )
+
+internal val ENERGY_AGGREGATION_METRIC_TYPE_MAP:
+ Map<AggregateMetric<Energy>, PlatformAggregateMetric<PlatformEnergy>> =
+ mapOf(
+ ActiveCaloriesBurnedRecord.ACTIVE_CALORIES_TOTAL to
+ PlatformActiveCaloriesBurnedRecord.ACTIVE_CALORIES_TOTAL,
+ BasalMetabolicRateRecord.BASAL_CALORIES_TOTAL to
+ PlatformBasalMetabolicRateRecord.BASAL_CALORIES_TOTAL,
+ NutritionRecord.ENERGY_TOTAL to PlatformNutritionRecord.ENERGY_TOTAL,
+ NutritionRecord.ENERGY_FROM_FAT_TOTAL to PlatformNutritionRecord.ENERGY_FROM_FAT_TOTAL,
+ TotalCaloriesBurnedRecord.ENERGY_TOTAL to PlatformTotalCaloriesBurnedRecord.ENERGY_TOTAL,
+ )
+
+internal val LENGTH_AGGREGATION_METRIC_TYPE_MAP:
+ Map<AggregateMetric<Length>, PlatformAggregateMetric<PlatformLength>> =
+ mapOf(
+ DistanceRecord.DISTANCE_TOTAL to PlatformDistanceRecord.DISTANCE_TOTAL,
+ ElevationGainedRecord.ELEVATION_GAINED_TOTAL to
+ PlatformElevationGainedRecord.ELEVATION_GAINED_TOTAL,
+ HeightRecord.HEIGHT_AVG to PlatformHeightRecord.HEIGHT_AVG,
+ HeightRecord.HEIGHT_MIN to PlatformHeightRecord.HEIGHT_MIN,
+ HeightRecord.HEIGHT_MAX to PlatformHeightRecord.HEIGHT_MAX,
+ )
+
+internal val LONG_AGGREGATION_METRIC_TYPE_MAP:
+ Map<AggregateMetric<Long>, PlatformAggregateMetric<Long>> =
+ mapOf(
+ HeartRateRecord.BPM_AVG to PlatformHeartRateRecord.BPM_AVG,
+ HeartRateRecord.BPM_MIN to PlatformHeartRateRecord.BPM_MIN,
+ HeartRateRecord.BPM_MAX to PlatformHeartRateRecord.BPM_MAX,
+ HeartRateRecord.MEASUREMENTS_COUNT to PlatformHeartRateRecord.HEART_MEASUREMENTS_COUNT,
+ RestingHeartRateRecord.BPM_AVG to PlatformRestingHeartRateRecord.BPM_AVG,
+ RestingHeartRateRecord.BPM_MIN to PlatformRestingHeartRateRecord.BPM_MIN,
+ RestingHeartRateRecord.BPM_MAX to PlatformRestingHeartRateRecord.BPM_MAX,
+ StepsRecord.COUNT_TOTAL to PlatformStepsRecord.STEPS_COUNT_TOTAL,
+ WheelchairPushesRecord.COUNT_TOTAL to
+ PlatformWheelchairPushesRecord.WHEEL_CHAIR_PUSHES_COUNT_TOTAL,
+ )
+
+internal val MASS_AGGREGATION_METRIC_TYPE_MAP:
+ Map<AggregateMetric<Mass>, PlatformAggregateMetric<PlatformMass>> =
+ mapOf(
+ NutritionRecord.BIOTIN_TOTAL to PlatformNutritionRecord.BIOTIN_TOTAL,
+ NutritionRecord.CAFFEINE_TOTAL to PlatformNutritionRecord.CAFFEINE_TOTAL,
+ NutritionRecord.CALCIUM_TOTAL to PlatformNutritionRecord.CALCIUM_TOTAL,
+ NutritionRecord.CHLORIDE_TOTAL to PlatformNutritionRecord.CHLORIDE_TOTAL,
+ NutritionRecord.CHOLESTEROL_TOTAL to PlatformNutritionRecord.CHOLESTEROL_TOTAL,
+ NutritionRecord.CHROMIUM_TOTAL to PlatformNutritionRecord.CHROMIUM_TOTAL,
+ NutritionRecord.COPPER_TOTAL to PlatformNutritionRecord.COPPER_TOTAL,
+ NutritionRecord.DIETARY_FIBER_TOTAL to PlatformNutritionRecord.DIETARY_FIBER_TOTAL,
+ NutritionRecord.FOLATE_TOTAL to PlatformNutritionRecord.FOLATE_TOTAL,
+ NutritionRecord.FOLIC_ACID_TOTAL to PlatformNutritionRecord.FOLIC_ACID_TOTAL,
+ NutritionRecord.IODINE_TOTAL to PlatformNutritionRecord.IODINE_TOTAL,
+ NutritionRecord.IRON_TOTAL to PlatformNutritionRecord.IRON_TOTAL,
+ NutritionRecord.MAGNESIUM_TOTAL to PlatformNutritionRecord.MAGNESIUM_TOTAL,
+ NutritionRecord.MANGANESE_TOTAL to PlatformNutritionRecord.MANGANESE_TOTAL,
+ NutritionRecord.MOLYBDENUM_TOTAL to PlatformNutritionRecord.MOLYBDENUM_TOTAL,
+ NutritionRecord.MONOUNSATURATED_FAT_TOTAL to
+ PlatformNutritionRecord.MONOUNSATURATED_FAT_TOTAL,
+ NutritionRecord.NIACIN_TOTAL to PlatformNutritionRecord.NIACIN_TOTAL,
+ NutritionRecord.PANTOTHENIC_ACID_TOTAL to PlatformNutritionRecord.PANTOTHENIC_ACID_TOTAL,
+ NutritionRecord.PHOSPHORUS_TOTAL to PlatformNutritionRecord.PHOSPHORUS_TOTAL,
+ NutritionRecord.POLYUNSATURATED_FAT_TOTAL to
+ PlatformNutritionRecord.POLYUNSATURATED_FAT_TOTAL,
+ NutritionRecord.POTASSIUM_TOTAL to PlatformNutritionRecord.POTASSIUM_TOTAL,
+ NutritionRecord.PROTEIN_TOTAL to PlatformNutritionRecord.PROTEIN_TOTAL,
+ NutritionRecord.RIBOFLAVIN_TOTAL to PlatformNutritionRecord.RIBOFLAVIN_TOTAL,
+ NutritionRecord.SATURATED_FAT_TOTAL to PlatformNutritionRecord.SATURATED_FAT_TOTAL,
+ NutritionRecord.SELENIUM_TOTAL to PlatformNutritionRecord.SELENIUM_TOTAL,
+ NutritionRecord.SODIUM_TOTAL to PlatformNutritionRecord.SODIUM_TOTAL,
+ NutritionRecord.SUGAR_TOTAL to PlatformNutritionRecord.SUGAR_TOTAL,
+ NutritionRecord.THIAMIN_TOTAL to PlatformNutritionRecord.THIAMIN_TOTAL,
+ NutritionRecord.TOTAL_CARBOHYDRATE_TOTAL to
+ PlatformNutritionRecord.TOTAL_CARBOHYDRATE_TOTAL,
+ NutritionRecord.TOTAL_FAT_TOTAL to PlatformNutritionRecord.TOTAL_FAT_TOTAL,
+ NutritionRecord.UNSATURATED_FAT_TOTAL to PlatformNutritionRecord.UNSATURATED_FAT_TOTAL,
+ NutritionRecord.VITAMIN_A_TOTAL to PlatformNutritionRecord.VITAMIN_A_TOTAL,
+ NutritionRecord.VITAMIN_B12_TOTAL to PlatformNutritionRecord.VITAMIN_B12_TOTAL,
+ NutritionRecord.VITAMIN_B6_TOTAL to PlatformNutritionRecord.VITAMIN_B6_TOTAL,
+ NutritionRecord.VITAMIN_C_TOTAL to PlatformNutritionRecord.VITAMIN_C_TOTAL,
+ NutritionRecord.VITAMIN_D_TOTAL to PlatformNutritionRecord.VITAMIN_D_TOTAL,
+ NutritionRecord.VITAMIN_E_TOTAL to PlatformNutritionRecord.VITAMIN_E_TOTAL,
+ NutritionRecord.VITAMIN_K_TOTAL to PlatformNutritionRecord.VITAMIN_K_TOTAL,
+ NutritionRecord.ZINC_TOTAL to PlatformNutritionRecord.ZINC_TOTAL,
+ WeightRecord.WEIGHT_AVG to PlatformWeightRecord.WEIGHT_AVG,
+ WeightRecord.WEIGHT_MIN to PlatformWeightRecord.WEIGHT_MIN,
+ WeightRecord.WEIGHT_MAX to PlatformWeightRecord.WEIGHT_MAX,
+ )
+
+internal val POWER_AGGREGATION_METRIC_TYPE_MAP:
+ Map<AggregateMetric<Power>, PlatformAggregateMetric<PlatformPower>> =
+ mapOf(
+ PowerRecord.POWER_AVG to PlatformPowerRecord.POWER_AVG,
+ PowerRecord.POWER_MAX to PlatformPowerRecord.POWER_MAX,
+ PowerRecord.POWER_MIN to PlatformPowerRecord.POWER_MIN,
+ )
+
+internal val VOLUME_AGGREGATION_METRIC_TYPE_MAP:
+ Map<AggregateMetric<Volume>, PlatformAggregateMetric<PlatformVolume>> =
+ mapOf(
+ HydrationRecord.VOLUME_TOTAL to PlatformHydrationRecord.VOLUME_TOTAL,
+ )
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/IntDefMappings.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/IntDefMappings.kt
new file mode 100644
index 0000000..67d896a
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/IntDefMappings.kt
@@ -0,0 +1,466 @@
+/*
+ * 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:RestrictTo(RestrictTo.Scope.LIBRARY)
+@file:RequiresApi(api = 34)
+
+package androidx.health.connect.client.impl.platform.records
+
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.records.BloodGlucoseRecord
+import androidx.health.connect.client.records.BloodPressureRecord
+import androidx.health.connect.client.records.BodyTemperatureMeasurementLocation
+import androidx.health.connect.client.records.CervicalMucusRecord
+import androidx.health.connect.client.records.ExerciseSessionRecord
+import androidx.health.connect.client.records.MealType
+import androidx.health.connect.client.records.MenstruationFlowRecord
+import androidx.health.connect.client.records.OvulationTestRecord
+import androidx.health.connect.client.records.SexualActivityRecord
+import androidx.health.connect.client.records.Vo2MaxRecord
+
+internal val SDK_TO_PLATFORM_CERVICAL_MUCUS_APPEARANCE: Map<Int, Int> =
+ mapOf(
+ CervicalMucusRecord.APPEARANCE_EGG_WHITE to
+ PlatformCervicalMucusAppearance.APPEARANCE_EGG_WHITE,
+ CervicalMucusRecord.APPEARANCE_DRY to PlatformCervicalMucusAppearance.APPEARANCE_DRY,
+ CervicalMucusRecord.APPEARANCE_STICKY to PlatformCervicalMucusAppearance.APPEARANCE_STICKY,
+ CervicalMucusRecord.APPEARANCE_CREAMY to PlatformCervicalMucusAppearance.APPEARANCE_CREAMY,
+ CervicalMucusRecord.APPEARANCE_WATERY to PlatformCervicalMucusAppearance.APPEARANCE_WATERY,
+ CervicalMucusRecord.APPEARANCE_UNUSUAL to
+ PlatformCervicalMucusAppearance.APPEARANCE_UNUSUAL,
+ )
+
+internal val PLATFORM_TO_SDK_CERVICAL_MUCUS_APPEARANCE =
+ SDK_TO_PLATFORM_CERVICAL_MUCUS_APPEARANCE.reversed()
+
+internal val SDK_TO_PLATFORM_BLOOD_PRESSURE_BODY_POSITION: Map<Int, Int> =
+ mapOf(
+ BloodPressureRecord.BODY_POSITION_STANDING_UP to
+ PlatformBloodPressureBodyPosition.BODY_POSITION_STANDING_UP,
+ BloodPressureRecord.BODY_POSITION_SITTING_DOWN to
+ PlatformBloodPressureBodyPosition.BODY_POSITION_SITTING_DOWN,
+ BloodPressureRecord.BODY_POSITION_LYING_DOWN to
+ PlatformBloodPressureBodyPosition.BODY_POSITION_LYING_DOWN,
+ BloodPressureRecord.BODY_POSITION_RECLINING to
+ PlatformBloodPressureBodyPosition.BODY_POSITION_RECLINING,
+ )
+
+internal val PLATFORM_TO_SDK_BLOOD_PRESSURE_BODY_POSITION =
+ SDK_TO_PLATFORM_BLOOD_PRESSURE_BODY_POSITION.reversed()
+
+internal val SDK_TO_PLATFORM_EXERCISE_SESSION_TYPE: Map<Int, Int> =
+ mapOf(
+ ExerciseSessionRecord.EXERCISE_TYPE_OTHER_WORKOUT to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_OTHER_WORKOUT,
+ ExerciseSessionRecord.EXERCISE_TYPE_BADMINTON to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_BADMINTON,
+ ExerciseSessionRecord.EXERCISE_TYPE_BASEBALL to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_BASEBALL,
+ ExerciseSessionRecord.EXERCISE_TYPE_BASKETBALL to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_BASKETBALL,
+ ExerciseSessionRecord.EXERCISE_TYPE_BIKING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_BIKING,
+ ExerciseSessionRecord.EXERCISE_TYPE_BIKING_STATIONARY to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_BIKING_STATIONARY,
+ ExerciseSessionRecord.EXERCISE_TYPE_BOOT_CAMP to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_BOOT_CAMP,
+ ExerciseSessionRecord.EXERCISE_TYPE_BOXING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_BOXING,
+ ExerciseSessionRecord.EXERCISE_TYPE_CALISTHENICS to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_CALISTHENICS,
+ ExerciseSessionRecord.EXERCISE_TYPE_CRICKET to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_CRICKET,
+ ExerciseSessionRecord.EXERCISE_TYPE_DANCING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_DANCING,
+ ExerciseSessionRecord.EXERCISE_TYPE_ELLIPTICAL to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_ELLIPTICAL,
+ ExerciseSessionRecord.EXERCISE_TYPE_EXERCISE_CLASS to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_EXERCISE_CLASS,
+ ExerciseSessionRecord.EXERCISE_TYPE_FENCING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_FENCING,
+ ExerciseSessionRecord.EXERCISE_TYPE_FOOTBALL_AMERICAN to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_FOOTBALL_AMERICAN,
+ ExerciseSessionRecord.EXERCISE_TYPE_FOOTBALL_AUSTRALIAN to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_FOOTBALL_AUSTRALIAN,
+ ExerciseSessionRecord.EXERCISE_TYPE_FRISBEE_DISC to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_FRISBEE_DISC,
+ ExerciseSessionRecord.EXERCISE_TYPE_GOLF to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_GOLF,
+ ExerciseSessionRecord.EXERCISE_TYPE_GUIDED_BREATHING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_GUIDED_BREATHING,
+ ExerciseSessionRecord.EXERCISE_TYPE_GYMNASTICS to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_GYMNASTICS,
+ ExerciseSessionRecord.EXERCISE_TYPE_HANDBALL to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_HANDBALL,
+ ExerciseSessionRecord.EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING,
+ ExerciseSessionRecord.EXERCISE_TYPE_HIKING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_HIKING,
+ ExerciseSessionRecord.EXERCISE_TYPE_ICE_HOCKEY to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_ICE_HOCKEY,
+ ExerciseSessionRecord.EXERCISE_TYPE_ICE_SKATING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_ICE_SKATING,
+ ExerciseSessionRecord.EXERCISE_TYPE_MARTIAL_ARTS to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_MARTIAL_ARTS,
+ ExerciseSessionRecord.EXERCISE_TYPE_PADDLING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_PADDLING,
+ ExerciseSessionRecord.EXERCISE_TYPE_PARAGLIDING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_PARAGLIDING,
+ ExerciseSessionRecord.EXERCISE_TYPE_PILATES to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_PILATES,
+ ExerciseSessionRecord.EXERCISE_TYPE_RACQUETBALL to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_RACQUETBALL,
+ ExerciseSessionRecord.EXERCISE_TYPE_ROCK_CLIMBING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_ROCK_CLIMBING,
+ ExerciseSessionRecord.EXERCISE_TYPE_ROLLER_HOCKEY to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_ROLLER_HOCKEY,
+ ExerciseSessionRecord.EXERCISE_TYPE_ROWING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_ROWING,
+ ExerciseSessionRecord.EXERCISE_TYPE_ROWING_MACHINE to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_ROWING_MACHINE,
+ ExerciseSessionRecord.EXERCISE_TYPE_RUGBY to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_RUGBY,
+ ExerciseSessionRecord.EXERCISE_TYPE_RUNNING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_RUNNING,
+ ExerciseSessionRecord.EXERCISE_TYPE_RUNNING_TREADMILL to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_RUNNING_TREADMILL,
+ ExerciseSessionRecord.EXERCISE_TYPE_SAILING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_SAILING,
+ ExerciseSessionRecord.EXERCISE_TYPE_SCUBA_DIVING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_SCUBA_DIVING,
+ ExerciseSessionRecord.EXERCISE_TYPE_SKATING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_SKATING,
+ ExerciseSessionRecord.EXERCISE_TYPE_SKIING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_SKIING,
+ ExerciseSessionRecord.EXERCISE_TYPE_SNOWBOARDING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_SNOWBOARDING,
+ ExerciseSessionRecord.EXERCISE_TYPE_SNOWSHOEING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_SNOWSHOEING,
+ ExerciseSessionRecord.EXERCISE_TYPE_SOCCER to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_SOCCER,
+ ExerciseSessionRecord.EXERCISE_TYPE_SOFTBALL to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_SOFTBALL,
+ ExerciseSessionRecord.EXERCISE_TYPE_SQUASH to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_SQUASH,
+ ExerciseSessionRecord.EXERCISE_TYPE_STAIR_CLIMBING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_STAIR_CLIMBING,
+ ExerciseSessionRecord.EXERCISE_TYPE_STAIR_CLIMBING_MACHINE to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_STAIR_CLIMBING_MACHINE,
+ ExerciseSessionRecord.EXERCISE_TYPE_STRENGTH_TRAINING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_STRENGTH_TRAINING,
+ ExerciseSessionRecord.EXERCISE_TYPE_STRETCHING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_STRETCHING,
+ ExerciseSessionRecord.EXERCISE_TYPE_SURFING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_SURFING,
+ ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_OPEN_WATER to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_SWIMMING_OPEN_WATER,
+ ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_POOL to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_SWIMMING_POOL,
+ ExerciseSessionRecord.EXERCISE_TYPE_TABLE_TENNIS to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_TABLE_TENNIS,
+ ExerciseSessionRecord.EXERCISE_TYPE_TENNIS to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_TENNIS,
+ ExerciseSessionRecord.EXERCISE_TYPE_VOLLEYBALL to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_VOLLEYBALL,
+ ExerciseSessionRecord.EXERCISE_TYPE_WALKING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_WALKING,
+ ExerciseSessionRecord.EXERCISE_TYPE_WATER_POLO to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_WATER_POLO,
+ ExerciseSessionRecord.EXERCISE_TYPE_WEIGHTLIFTING to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_WEIGHTLIFTING,
+ ExerciseSessionRecord.EXERCISE_TYPE_WHEELCHAIR to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_WHEELCHAIR,
+ ExerciseSessionRecord.EXERCISE_TYPE_YOGA to
+ PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_YOGA,
+ )
+
+internal val PLATFORM_TO_SDK_EXERCISE_SESSION_TYPE =
+ SDK_TO_PLATFORM_EXERCISE_SESSION_TYPE.reversed()
+
+internal val SDK_TO_PLATFORM_MEAL_TYPE: Map<Int, Int> =
+ mapOf(
+ MealType.MEAL_TYPE_BREAKFAST to PlatformMealType.MEAL_TYPE_BREAKFAST,
+ MealType.MEAL_TYPE_LUNCH to PlatformMealType.MEAL_TYPE_LUNCH,
+ MealType.MEAL_TYPE_DINNER to PlatformMealType.MEAL_TYPE_DINNER,
+ MealType.MEAL_TYPE_SNACK to PlatformMealType.MEAL_TYPE_SNACK,
+ )
+
+internal val PLATFORM_TO_SDK_MEAL_TYPE = SDK_TO_PLATFORM_MEAL_TYPE.reversed()
+
+internal val SDK_TO_PLATFORM_VO2_MAX_MEASUREMENT_METHOD: Map<Int, Int> =
+ mapOf(
+ Vo2MaxRecord.MEASUREMENT_METHOD_METABOLIC_CART to
+ PlatformVo2MaxMeasurementMethod.MEASUREMENT_METHOD_METABOLIC_CART,
+ Vo2MaxRecord.MEASUREMENT_METHOD_HEART_RATE_RATIO to
+ PlatformVo2MaxMeasurementMethod.MEASUREMENT_METHOD_HEART_RATE_RATIO,
+ Vo2MaxRecord.MEASUREMENT_METHOD_COOPER_TEST to
+ PlatformVo2MaxMeasurementMethod.MEASUREMENT_METHOD_COOPER_TEST,
+ Vo2MaxRecord.MEASUREMENT_METHOD_MULTISTAGE_FITNESS_TEST to
+ PlatformVo2MaxMeasurementMethod.MEASUREMENT_METHOD_MULTISTAGE_FITNESS_TEST,
+ Vo2MaxRecord.MEASUREMENT_METHOD_ROCKPORT_FITNESS_TEST to
+ PlatformVo2MaxMeasurementMethod.MEASUREMENT_METHOD_ROCKPORT_FITNESS_TEST,
+ )
+
+internal val PLATFORM_TO_SDK_VO2_MAX_MEASUREMENT_METHOD =
+ SDK_TO_PLATFORM_VO2_MAX_MEASUREMENT_METHOD.reversed()
+
+internal val SDK_TO_PLATFORM_MENSTRUATION_FLOW_TYPE: Map<Int, Int> =
+ mapOf(
+ MenstruationFlowRecord.FLOW_LIGHT to PlatformMenstruationFlowType.FLOW_LIGHT,
+ MenstruationFlowRecord.FLOW_MEDIUM to PlatformMenstruationFlowType.FLOW_MEDIUM,
+ MenstruationFlowRecord.FLOW_HEAVY to PlatformMenstruationFlowType.FLOW_HEAVY,
+ )
+
+internal val PLATFORM_TO_SDK_MENSTRUATION_FLOW_TYPE =
+ SDK_TO_PLATFORM_MENSTRUATION_FLOW_TYPE.reversed()
+
+internal val SDK_TO_PLATFORM_BODY_TEMPERATURE_MEASUREMENT_LOCATION: Map<Int, Int> =
+ mapOf(
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_ARMPIT to
+ PlatformBodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_ARMPIT,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_FINGER to
+ PlatformBodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_FINGER,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_FOREHEAD to
+ PlatformBodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_FOREHEAD,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_MOUTH to
+ PlatformBodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_MOUTH,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_RECTUM to
+ PlatformBodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_RECTUM,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_TEMPORAL_ARTERY to
+ PlatformBodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_TEMPORAL_ARTERY,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_TOE to
+ PlatformBodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_TOE,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_EAR to
+ PlatformBodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_EAR,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_WRIST to
+ PlatformBodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_WRIST,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_VAGINA to
+ PlatformBodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_VAGINA,
+ )
+
+internal val PLATFORM_TO_SDK_BODY_TEMPERATURE_MEASUREMENT_LOCATION =
+ SDK_TO_PLATFORM_BODY_TEMPERATURE_MEASUREMENT_LOCATION.reversed()
+
+internal val SDK_TO_PLATFORM_BLOOD_PRESSURE_MEASUREMENT_LOCATION: Map<Int, Int> =
+ mapOf(
+ BloodPressureRecord.MEASUREMENT_LOCATION_LEFT_WRIST to
+ PlatformBloodPressureMeasurementLocation.BLOOD_PRESSURE_MEASUREMENT_LOCATION_LEFT_WRIST,
+ BloodPressureRecord.MEASUREMENT_LOCATION_RIGHT_WRIST to
+ PlatformBloodPressureMeasurementLocation
+ .BLOOD_PRESSURE_MEASUREMENT_LOCATION_RIGHT_WRIST,
+ BloodPressureRecord.MEASUREMENT_LOCATION_LEFT_UPPER_ARM to
+ PlatformBloodPressureMeasurementLocation
+ .BLOOD_PRESSURE_MEASUREMENT_LOCATION_LEFT_UPPER_ARM,
+ BloodPressureRecord.MEASUREMENT_LOCATION_RIGHT_UPPER_ARM to
+ PlatformBloodPressureMeasurementLocation
+ .BLOOD_PRESSURE_MEASUREMENT_LOCATION_RIGHT_UPPER_ARM,
+ )
+
+internal val PLATFORM_TO_SDK_BLOOD_PRESSURE_MEASUREMENT_LOCATION =
+ SDK_TO_PLATFORM_BLOOD_PRESSURE_MEASUREMENT_LOCATION.reversed()
+
+internal val SDK_TO_PLATFORM_OVULATION_TEST_RESULT: Map<Int, Int> =
+ mapOf(
+ OvulationTestRecord.RESULT_POSITIVE to PlatformOvulationTestResult.RESULT_POSITIVE,
+ OvulationTestRecord.RESULT_HIGH to PlatformOvulationTestResult.RESULT_HIGH,
+ OvulationTestRecord.RESULT_NEGATIVE to PlatformOvulationTestResult.RESULT_NEGATIVE,
+ OvulationTestRecord.RESULT_INCONCLUSIVE to PlatformOvulationTestResult.RESULT_INCONCLUSIVE,
+ )
+
+internal val PLATFORM_TO_SDK_OVULATION_TEST_RESULT =
+ SDK_TO_PLATFORM_OVULATION_TEST_RESULT.reversed()
+
+internal val SDK_TO_PLATFORM_CERVICAL_MUCUS_SENSATION: Map<Int, Int> =
+ mapOf(
+ CervicalMucusRecord.SENSATION_LIGHT to PlatformCervicalMucusSensation.SENSATION_LIGHT,
+ CervicalMucusRecord.SENSATION_MEDIUM to PlatformCervicalMucusSensation.SENSATION_MEDIUM,
+ CervicalMucusRecord.SENSATION_HEAVY to PlatformCervicalMucusSensation.SENSATION_HEAVY,
+ )
+
+internal val PLATFORM_TO_SDK_CERVICAL_MUCUS_SENSATION =
+ SDK_TO_PLATFORM_CERVICAL_MUCUS_SENSATION.reversed()
+
+internal val SDK_TO_PLATFORM_SEXUAL_ACTIVITY_PROTECTION_USED: Map<Int, Int> =
+ mapOf(
+ SexualActivityRecord.PROTECTION_USED_PROTECTED to
+ PlatformSexualActivityProtectionUsed.PROTECTION_USED_PROTECTED,
+ SexualActivityRecord.PROTECTION_USED_UNPROTECTED to
+ PlatformSexualActivityProtectionUsed.PROTECTION_USED_UNPROTECTED,
+ )
+
+internal val PLATFORM_TO_SDK_SEXUAL_ACTIVITY_PROTECTION_USED =
+ SDK_TO_PLATFORM_SEXUAL_ACTIVITY_PROTECTION_USED.reversed()
+
+internal val SDK_TO_PLATFORM_BLOOD_GLUCOSE_SPECIMEN_SOURCE: Map<Int, Int> =
+ mapOf(
+ BloodGlucoseRecord.SPECIMEN_SOURCE_INTERSTITIAL_FLUID to
+ PlatformBloodGlucoseSpecimenSource.SPECIMEN_SOURCE_INTERSTITIAL_FLUID,
+ BloodGlucoseRecord.SPECIMEN_SOURCE_CAPILLARY_BLOOD to
+ PlatformBloodGlucoseSpecimenSource.SPECIMEN_SOURCE_CAPILLARY_BLOOD,
+ BloodGlucoseRecord.SPECIMEN_SOURCE_PLASMA to
+ PlatformBloodGlucoseSpecimenSource.SPECIMEN_SOURCE_PLASMA,
+ BloodGlucoseRecord.SPECIMEN_SOURCE_SERUM to
+ PlatformBloodGlucoseSpecimenSource.SPECIMEN_SOURCE_SERUM,
+ BloodGlucoseRecord.SPECIMEN_SOURCE_TEARS to
+ PlatformBloodGlucoseSpecimenSource.SPECIMEN_SOURCE_TEARS,
+ BloodGlucoseRecord.SPECIMEN_SOURCE_WHOLE_BLOOD to
+ PlatformBloodGlucoseSpecimenSource.SPECIMEN_SOURCE_WHOLE_BLOOD,
+ )
+
+internal val PLATFORM_TO_SDK_GLUCOSE_SPECIMEN_SOURCE =
+ SDK_TO_PLATFORM_BLOOD_GLUCOSE_SPECIMEN_SOURCE.reversed()
+
+internal val SDK_TO_PLATFORM_BLOOD_GLUCOSE_RELATION_TO_MEAL: Map<Int, Int> =
+ mapOf(
+ BloodGlucoseRecord.RELATION_TO_MEAL_GENERAL to
+ PlatformBloodGlucoseRelationToMeal.RELATION_TO_MEAL_GENERAL,
+ BloodGlucoseRecord.RELATION_TO_MEAL_FASTING to
+ PlatformBloodGlucoseRelationToMeal.RELATION_TO_MEAL_FASTING,
+ BloodGlucoseRecord.RELATION_TO_MEAL_BEFORE_MEAL to
+ PlatformBloodGlucoseRelationToMeal.RELATION_TO_MEAL_BEFORE_MEAL,
+ BloodGlucoseRecord.RELATION_TO_MEAL_AFTER_MEAL to
+ PlatformBloodGlucoseRelationToMeal.RELATION_TO_MEAL_AFTER_MEAL,
+ )
+
+internal val PLATFORM_TO_SDK_BLOOD_GLUCOSE_RELATION_TO_MEAL =
+ SDK_TO_PLATFORM_BLOOD_GLUCOSE_RELATION_TO_MEAL
+
+internal fun Int.toPlatformCervicalMucusAppearance(): Int {
+ return SDK_TO_PLATFORM_CERVICAL_MUCUS_APPEARANCE[this]
+ ?: PlatformCervicalMucusAppearance.APPEARANCE_UNKNOWN
+}
+
+internal fun Int.toPlatformBloodPressureBodyPosition(): Int {
+ return SDK_TO_PLATFORM_BLOOD_PRESSURE_BODY_POSITION[this]
+ ?: PlatformBloodPressureBodyPosition.BODY_POSITION_UNKNOWN
+}
+
+internal fun Int.toPlatformExerciseSessionType(): Int {
+ return SDK_TO_PLATFORM_EXERCISE_SESSION_TYPE[this]
+ ?: PlatformExerciseSessionType.EXERCISE_SESSION_TYPE_UNKNOWN
+}
+
+internal fun Int.toPlatformMealType(): Int {
+ return SDK_TO_PLATFORM_MEAL_TYPE[this] ?: PlatformMealType.MEAL_TYPE_UNKNOWN
+}
+
+internal fun Int.toPlatformVo2MaxMeasurementMethod(): Int {
+ return SDK_TO_PLATFORM_VO2_MAX_MEASUREMENT_METHOD[this]
+ ?: PlatformVo2MaxMeasurementMethod.MEASUREMENT_METHOD_OTHER
+}
+
+internal fun Int.toPlatformMenstruationFlow(): Int {
+ return SDK_TO_PLATFORM_MENSTRUATION_FLOW_TYPE[this] ?: PlatformMenstruationFlowType.FLOW_UNKNOWN
+}
+
+internal fun Int.toPlatformBodyTemperatureMeasurementLocation(): Int {
+ return SDK_TO_PLATFORM_BODY_TEMPERATURE_MEASUREMENT_LOCATION[this]
+ ?: PlatformBodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_UNKNOWN
+}
+
+internal fun Int.toPlatformBloodPressureMeasurementLocation(): Int {
+ return SDK_TO_PLATFORM_BLOOD_PRESSURE_MEASUREMENT_LOCATION[this]
+ ?: PlatformBloodPressureMeasurementLocation.BLOOD_PRESSURE_MEASUREMENT_LOCATION_UNKNOWN
+}
+
+internal fun Int.toPlatformOvulationTestResult(): Int {
+ return SDK_TO_PLATFORM_OVULATION_TEST_RESULT[this]
+ ?: PlatformOvulationTestResult.RESULT_INCONCLUSIVE
+}
+
+internal fun Int.toPlatformCervicalMucusSensation(): Int {
+ return SDK_TO_PLATFORM_CERVICAL_MUCUS_SENSATION[this]
+ ?: PlatformCervicalMucusSensation.SENSATION_UNKNOWN
+}
+
+internal fun Int.toPlatformSexualActivityProtectionUsed(): Int {
+ return SDK_TO_PLATFORM_SEXUAL_ACTIVITY_PROTECTION_USED[this]
+ ?: PlatformSexualActivityProtectionUsed.PROTECTION_USED_UNKNOWN
+}
+
+internal fun Int.toPlatformBloodGlucoseSpecimenSource(): Int {
+ return SDK_TO_PLATFORM_BLOOD_GLUCOSE_SPECIMEN_SOURCE[this]
+ ?: PlatformBloodGlucoseSpecimenSource.SPECIMEN_SOURCE_UNKNOWN
+}
+
+internal fun Int.toPlatformBloodGlucoseRelationToMeal(): Int {
+ return SDK_TO_PLATFORM_BLOOD_GLUCOSE_RELATION_TO_MEAL[this]
+ ?: PlatformBloodGlucoseRelationToMeal.RELATION_TO_MEAL_UNKNOWN
+}
+
+internal fun Int.toSdkBloodPressureBodyPosition(): Int {
+ return PLATFORM_TO_SDK_BLOOD_PRESSURE_BODY_POSITION[this]
+ ?: BloodPressureRecord.BODY_POSITION_UNKNOWN
+}
+
+internal fun Int.toSdkBloodPressureMeasurementLocation(): Int {
+ return PLATFORM_TO_SDK_BLOOD_PRESSURE_MEASUREMENT_LOCATION[this]
+ ?: BloodPressureRecord.MEASUREMENT_LOCATION_UNKNOWN
+}
+
+internal fun Int.toSdkExerciseSessionType(): Int {
+ return PLATFORM_TO_SDK_EXERCISE_SESSION_TYPE[this]
+ ?: ExerciseSessionRecord.EXERCISE_TYPE_OTHER_WORKOUT
+}
+
+internal fun Int.toSdkVo2MaxMeasurementMethod(): Int {
+ return PLATFORM_TO_SDK_VO2_MAX_MEASUREMENT_METHOD[this] ?: Vo2MaxRecord.MEASUREMENT_METHOD_OTHER
+}
+
+internal fun Int.toSdkMenstruationFlow(): Int {
+ return PLATFORM_TO_SDK_MENSTRUATION_FLOW_TYPE[this] ?: MenstruationFlowRecord.FLOW_UNKNOWN
+}
+
+internal fun Int.toSdkProtectionUsed(): Int {
+ return PLATFORM_TO_SDK_SEXUAL_ACTIVITY_PROTECTION_USED[this]
+ ?: SexualActivityRecord.PROTECTION_USED_UNKNOWN
+}
+
+internal fun Int.toSdkCervicalMucusSensation(): Int {
+ return PLATFORM_TO_SDK_CERVICAL_MUCUS_SENSATION[this] ?: CervicalMucusRecord.SENSATION_UNKNOWN
+}
+
+internal fun Int.toSdkBloodGlucoseSpecimenSource(): Int {
+ return PLATFORM_TO_SDK_GLUCOSE_SPECIMEN_SOURCE[this]
+ ?: BloodGlucoseRecord.SPECIMEN_SOURCE_UNKNOWN
+}
+
+internal fun Int.toSdkMealType(): Int {
+ return PLATFORM_TO_SDK_MEAL_TYPE[this] ?: MealType.MEAL_TYPE_UNKNOWN
+}
+
+internal fun Int.toSdkOvulationTestResult(): Int {
+ return PLATFORM_TO_SDK_OVULATION_TEST_RESULT[this] ?: OvulationTestRecord.RESULT_INCONCLUSIVE
+}
+
+internal fun Int.toSdkRelationToMeal(): Int {
+ return PLATFORM_TO_SDK_BLOOD_GLUCOSE_RELATION_TO_MEAL[this]
+ ?: BloodGlucoseRecord.RELATION_TO_MEAL_UNKNOWN
+}
+
+internal fun Int.toSdkBodyTemperatureMeasurementLocation(): Int {
+ return PLATFORM_TO_SDK_BODY_TEMPERATURE_MEASUREMENT_LOCATION[this]
+ ?: BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_UNKNOWN
+}
+
+internal fun Int.toSdkCervicalMucusAppearance(): Int {
+ return PLATFORM_TO_SDK_CERVICAL_MUCUS_APPEARANCE[this] ?: CervicalMucusRecord.APPEARANCE_UNKNOWN
+}
+
+private fun Map<Int, Int>.reversed(): Map<Int, Int> {
+ return entries.associate { (k, v) -> v to k }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/MetadataConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/MetadataConverters.kt
new file mode 100644
index 0000000..97352c8
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/MetadataConverters.kt
@@ -0,0 +1,74 @@
+/*
+ * 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:RestrictTo(RestrictTo.Scope.LIBRARY)
+@file:RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+
+package androidx.health.connect.client.impl.platform.records
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.records.metadata.DataOrigin
+import androidx.health.connect.client.records.metadata.Device
+import androidx.health.connect.client.records.metadata.Metadata
+
+internal fun PlatformMetadata.toSdkMetadata(): Metadata {
+ return Metadata(
+ id = id,
+ dataOrigin = dataOrigin.toSdkDataOrigin(),
+ lastModifiedTime = lastModifiedTime,
+ clientRecordId = clientRecordId,
+ clientRecordVersion = clientRecordVersion,
+ device = device.toSdkDevice())
+}
+
+internal fun PlatformDevice.toSdkDevice(): Device {
+ @Suppress("WrongConstant") // Platform intdef and jetpack intdef match in value.
+ return Device(manufacturer = manufacturer, model = model, type = type)
+}
+
+internal fun PlatformDataOrigin.toSdkDataOrigin(): DataOrigin {
+ return DataOrigin(packageName)
+}
+
+internal fun Metadata.toPlatformMetadata(): PlatformMetadata {
+ return PlatformMetadataBuilder()
+ .apply {
+ device?.toPlatformDevice()?.let { setDevice(it) }
+ setLastModifiedTime(lastModifiedTime)
+ setId(id)
+ setDataOrigin(dataOrigin.toPlatformDataOrigin())
+ setClientRecordId(clientRecordId)
+ setClientRecordVersion(clientRecordVersion)
+ }
+ .build()
+}
+
+internal fun DataOrigin.toPlatformDataOrigin(): PlatformDataOrigin {
+ return PlatformDataOriginBuilder().apply { setPackageName(packageName) }.build()
+}
+
+internal fun Device.toPlatformDevice(): PlatformDevice {
+ @Suppress("WrongConstant") // Platform intdef and jetpack intdef match in value.
+ return PlatformDeviceBuilder()
+ .apply {
+ setType(type)
+ manufacturer?.let { setManufacturer(it) }
+ model?.let { setModel(it) }
+ }
+ .build()
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/PlatformRecordAliases.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/PlatformRecordAliases.kt
new file mode 100644
index 0000000..45e88a0
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/PlatformRecordAliases.kt
@@ -0,0 +1,327 @@
+/*
+ * 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:RestrictTo(RestrictTo.Scope.LIBRARY)
+@file:RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+
+package androidx.health.connect.client.impl.platform.records
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+
+internal typealias PlatformInstantRecord = android.health.connect.datatypes.InstantRecord
+
+internal typealias PlatformIntervalRecord = android.health.connect.datatypes.IntervalRecord
+
+internal typealias PlatformRecord = android.health.connect.datatypes.Record
+
+internal typealias PlatformActiveCaloriesBurnedRecord =
+ android.health.connect.datatypes.ActiveCaloriesBurnedRecord
+
+internal typealias PlatformActiveCaloriesBurnedRecordBuilder =
+ android.health.connect.datatypes.ActiveCaloriesBurnedRecord.Builder
+
+internal typealias PlatformBasalBodyTemperatureRecord =
+ android.health.connect.datatypes.BasalBodyTemperatureRecord
+
+internal typealias PlatformBasalBodyTemperatureRecordBuilder =
+ android.health.connect.datatypes.BasalBodyTemperatureRecord.Builder
+
+internal typealias PlatformBodyTemperatureMeasurementLocation =
+ android.health.connect.datatypes.BodyTemperatureMeasurementLocation
+
+internal typealias PlatformBasalMetabolicRateRecord =
+ android.health.connect.datatypes.BasalMetabolicRateRecord
+
+internal typealias PlatformBasalMetabolicRateRecordBuilder =
+ android.health.connect.datatypes.BasalMetabolicRateRecord.Builder
+
+internal typealias PlatformBloodGlucoseRecord = android.health.connect.datatypes.BloodGlucoseRecord
+
+internal typealias PlatformBloodGlucoseRecordBuilder =
+ android.health.connect.datatypes.BloodGlucoseRecord.Builder
+
+internal typealias PlatformBloodGlucoseSpecimenSource =
+ android.health.connect.datatypes.BloodGlucoseRecord.SpecimenSource
+
+internal typealias PlatformBloodGlucoseRelationToMealType =
+ android.health.connect.datatypes.BloodGlucoseRecord.RelationToMealType
+
+internal typealias PlatformBloodPressureRecord =
+ android.health.connect.datatypes.BloodPressureRecord
+
+internal typealias PlatformBloodPressureRecordBuilder =
+ android.health.connect.datatypes.BloodPressureRecord.Builder
+
+internal typealias PlatformBloodGlucoseRelationToMeal =
+ android.health.connect.datatypes.BloodGlucoseRecord.RelationToMealType
+
+internal typealias PlatformBloodPressureBodyPosition =
+ android.health.connect.datatypes.BloodPressureRecord.BodyPosition
+
+internal typealias PlatformBloodPressureMeasurementLocation =
+ android.health.connect.datatypes.BloodPressureRecord.BloodPressureMeasurementLocation
+
+internal typealias PlatformBodyFatRecord = android.health.connect.datatypes.BodyFatRecord
+
+internal typealias PlatformBodyFatRecordBuilder =
+ android.health.connect.datatypes.BodyFatRecord.Builder
+
+internal typealias PlatformBodyTemperatureRecord =
+ android.health.connect.datatypes.BodyTemperatureRecord
+
+internal typealias PlatformBodyTemperatureRecordBuilder =
+ android.health.connect.datatypes.BodyTemperatureRecord.Builder
+
+internal typealias PlatformBodyWaterMassRecord =
+ android.health.connect.datatypes.BodyWaterMassRecord
+
+internal typealias PlatformBodyWaterMassRecordBuilder =
+ android.health.connect.datatypes.BodyWaterMassRecord.Builder
+
+internal typealias PlatformBoneMassRecord = android.health.connect.datatypes.BoneMassRecord
+
+internal typealias PlatformBoneMassRecordBuilder =
+ android.health.connect.datatypes.BoneMassRecord.Builder
+
+internal typealias PlatformCervicalMucusRecord =
+ android.health.connect.datatypes.CervicalMucusRecord
+
+internal typealias PlatformCervicalMucusRecordBuilder =
+ android.health.connect.datatypes.CervicalMucusRecord.Builder
+
+internal typealias PlatformCervicalMucusAppearance =
+ android.health.connect.datatypes.CervicalMucusRecord.CervicalMucusAppearance
+
+internal typealias PlatformCervicalMucusSensation =
+ android.health.connect.datatypes.CervicalMucusRecord.CervicalMucusSensation
+
+internal typealias PlatformCyclingPedalingCadenceRecord =
+ android.health.connect.datatypes.CyclingPedalingCadenceRecord
+
+internal typealias PlatformCyclingPedalingCadenceRecordBuilder =
+ android.health.connect.datatypes.CyclingPedalingCadenceRecord.Builder
+
+internal typealias PlatformCyclingPedalingCadenceSample =
+ android.health.connect.datatypes.CyclingPedalingCadenceRecord.CyclingPedalingCadenceRecordSample
+
+internal typealias PlatformDistanceRecord = android.health.connect.datatypes.DistanceRecord
+
+internal typealias PlatformDistanceRecordBuilder =
+ android.health.connect.datatypes.DistanceRecord.Builder
+
+internal typealias PlatformElevationGainedRecord =
+ android.health.connect.datatypes.ElevationGainedRecord
+
+internal typealias PlatformElevationGainedRecordBuilder =
+ android.health.connect.datatypes.ElevationGainedRecord.Builder
+
+internal typealias PlatformExerciseSessionRecord =
+ android.health.connect.datatypes.ExerciseSessionRecord
+
+internal typealias PlatformExerciseSessionRecordBuilder =
+ android.health.connect.datatypes.ExerciseSessionRecord.Builder
+
+internal typealias PlatformExerciseSessionType =
+ android.health.connect.datatypes.ExerciseSessionType
+
+internal typealias PlatformFloorsClimbedRecord =
+ android.health.connect.datatypes.FloorsClimbedRecord
+
+internal typealias PlatformFloorsClimbedRecordBuilder =
+ android.health.connect.datatypes.FloorsClimbedRecord.Builder
+
+internal typealias PlatformHeartRateRecord = android.health.connect.datatypes.HeartRateRecord
+
+internal typealias PlatformHeartRateRecordBuilder =
+ android.health.connect.datatypes.HeartRateRecord.Builder
+
+internal typealias PlatformHeartRateSample =
+ android.health.connect.datatypes.HeartRateRecord.HeartRateSample
+
+internal typealias PlatformHeartRateVariabilityRmssdRecord =
+ android.health.connect.datatypes.HeartRateVariabilityRmssdRecord
+
+internal typealias PlatformHeartRateVariabilityRmssdRecordBuilder =
+ android.health.connect.datatypes.HeartRateVariabilityRmssdRecord.Builder
+
+internal typealias PlatformHeightRecord = android.health.connect.datatypes.HeightRecord
+
+internal typealias PlatformHeightRecordBuilder =
+ android.health.connect.datatypes.HeightRecord.Builder
+
+internal typealias PlatformHydrationRecord = android.health.connect.datatypes.HydrationRecord
+
+internal typealias PlatformHydrationRecordBuilder =
+ android.health.connect.datatypes.HydrationRecord.Builder
+
+internal typealias PlatformIntermenstrualBleedingRecord =
+ android.health.connect.datatypes.IntermenstrualBleedingRecord
+
+internal typealias PlatformIntermenstrualBleedingRecordBuilder =
+ android.health.connect.datatypes.IntermenstrualBleedingRecord.Builder
+
+internal typealias PlatformLeanBodyMassRecord = android.health.connect.datatypes.LeanBodyMassRecord
+
+internal typealias PlatformLeanBodyMassRecordBuilder =
+ android.health.connect.datatypes.LeanBodyMassRecord.Builder
+
+internal typealias PlatformMenstruationFlowRecord =
+ android.health.connect.datatypes.MenstruationFlowRecord
+
+internal typealias PlatformMenstruationFlowRecordBuilder =
+ android.health.connect.datatypes.MenstruationFlowRecord.Builder
+
+internal typealias PlatformMenstruationFlowType =
+ android.health.connect.datatypes.MenstruationFlowRecord.MenstruationFlowType
+
+internal typealias PlatformMealType = android.health.connect.datatypes.MealType
+
+internal typealias PlatformMenstruationPeriodRecord =
+ android.health.connect.datatypes.MenstruationPeriodRecord
+
+internal typealias PlatformMenstruationPeriodRecordBuilder =
+ android.health.connect.datatypes.MenstruationPeriodRecord.Builder
+
+internal typealias PlatformNutritionRecord = android.health.connect.datatypes.NutritionRecord
+
+internal typealias PlatformNutritionRecordBuilder =
+ android.health.connect.datatypes.NutritionRecord.Builder
+
+internal typealias PlatformOvulationTestRecord =
+ android.health.connect.datatypes.OvulationTestRecord
+
+internal typealias PlatformOvulationTestRecordBuilder =
+ android.health.connect.datatypes.OvulationTestRecord.Builder
+
+internal typealias PlatformOvulationTestResult =
+ android.health.connect.datatypes.OvulationTestRecord.OvulationTestResult
+
+internal typealias PlatformOxygenSaturationRecord =
+ android.health.connect.datatypes.OxygenSaturationRecord
+
+internal typealias PlatformOxygenSaturationRecordBuilder =
+ android.health.connect.datatypes.OxygenSaturationRecord.Builder
+
+internal typealias PlatformPowerRecord = android.health.connect.datatypes.PowerRecord
+
+internal typealias PlatformPowerRecordBuilder = android.health.connect.datatypes.PowerRecord.Builder
+
+internal typealias PlatformPowerRecordSample =
+ android.health.connect.datatypes.PowerRecord.PowerRecordSample
+
+internal typealias PlatformRespiratoryRateRecord =
+ android.health.connect.datatypes.RespiratoryRateRecord
+
+internal typealias PlatformRespiratoryRateRecordBuilder =
+ android.health.connect.datatypes.RespiratoryRateRecord.Builder
+
+internal typealias PlatformRestingHeartRateRecord =
+ android.health.connect.datatypes.RestingHeartRateRecord
+
+internal typealias PlatformRestingHeartRateRecordBuilder =
+ android.health.connect.datatypes.RestingHeartRateRecord.Builder
+
+internal typealias PlatformSexualActivityRecord =
+ android.health.connect.datatypes.SexualActivityRecord
+
+internal typealias PlatformSexualActivityRecordBuilder =
+ android.health.connect.datatypes.SexualActivityRecord.Builder
+
+internal typealias PlatformSexualActivityProtectionUsed =
+ android.health.connect.datatypes.SexualActivityRecord.SexualActivityProtectionUsed
+
+internal typealias PlatformSleepSessionRecord = android.health.connect.datatypes.SleepSessionRecord
+
+internal typealias PlatformSleepSessionRecordBuilder =
+ android.health.connect.datatypes.SleepSessionRecord.Builder
+
+internal typealias PlatformSpeedRecord = android.health.connect.datatypes.SpeedRecord
+
+internal typealias PlatformSpeedRecordBuilder = android.health.connect.datatypes.SpeedRecord.Builder
+
+internal typealias PlatformSpeedSample =
+ android.health.connect.datatypes.SpeedRecord.SpeedRecordSample
+
+internal typealias PlatformStepsCadenceRecord = android.health.connect.datatypes.StepsCadenceRecord
+
+internal typealias PlatformStepsCadenceRecordBuilder =
+ android.health.connect.datatypes.StepsCadenceRecord.Builder
+
+internal typealias PlatformStepsCadenceSample =
+ android.health.connect.datatypes.StepsCadenceRecord.StepsCadenceRecordSample
+
+internal typealias PlatformStepsRecord = android.health.connect.datatypes.StepsRecord
+
+internal typealias PlatformStepsRecordBuilder = android.health.connect.datatypes.StepsRecord.Builder
+
+internal typealias PlatformTotalCaloriesBurnedRecord =
+ android.health.connect.datatypes.TotalCaloriesBurnedRecord
+
+internal typealias PlatformTotalCaloriesBurnedRecordBuilder =
+ android.health.connect.datatypes.TotalCaloriesBurnedRecord.Builder
+
+internal typealias PlatformVo2MaxRecord = android.health.connect.datatypes.Vo2MaxRecord
+
+internal typealias PlatformVo2MaxRecordBuilder =
+ android.health.connect.datatypes.Vo2MaxRecord.Builder
+
+internal typealias PlatformVo2MaxMeasurementMethod =
+ android.health.connect.datatypes.Vo2MaxRecord.Vo2MaxMeasurementMethod
+
+internal typealias PlatformWeightRecord = android.health.connect.datatypes.WeightRecord
+
+internal typealias PlatformWeightRecordBuilder =
+ android.health.connect.datatypes.WeightRecord.Builder
+
+internal typealias PlatformWheelchairPushesRecord =
+ android.health.connect.datatypes.WheelchairPushesRecord
+
+internal typealias PlatformWheelchairPushesRecordBuilder =
+ android.health.connect.datatypes.WheelchairPushesRecord.Builder
+
+internal typealias PlatformDataOrigin = android.health.connect.datatypes.DataOrigin
+
+internal typealias PlatformDataOriginBuilder = android.health.connect.datatypes.DataOrigin.Builder
+
+internal typealias PlatformDevice = android.health.connect.datatypes.Device
+
+internal typealias PlatformDeviceBuilder = android.health.connect.datatypes.Device.Builder
+
+internal typealias PlatformMetadata = android.health.connect.datatypes.Metadata
+
+internal typealias PlatformMetadataBuilder = android.health.connect.datatypes.Metadata.Builder
+
+internal typealias PlatformBloodGlucose = android.health.connect.datatypes.units.BloodGlucose
+
+internal typealias PlatformEnergy = android.health.connect.datatypes.units.Energy
+
+internal typealias PlatformLength = android.health.connect.datatypes.units.Length
+
+internal typealias PlatformMass = android.health.connect.datatypes.units.Mass
+
+internal typealias PlatformPercentage = android.health.connect.datatypes.units.Percentage
+
+internal typealias PlatformPower = android.health.connect.datatypes.units.Power
+
+internal typealias PlatformPressure = android.health.connect.datatypes.units.Pressure
+
+internal typealias PlatformTemperature = android.health.connect.datatypes.units.Temperature
+
+internal typealias PlatformVelocity = android.health.connect.datatypes.units.Velocity
+
+internal typealias PlatformVolume = android.health.connect.datatypes.units.Volume
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RecordConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RecordConverters.kt
new file mode 100644
index 0000000..186b7f3
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RecordConverters.kt
@@ -0,0 +1,979 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:RestrictTo(RestrictTo.Scope.LIBRARY)
+@file:RequiresApi(api = 34)
+
+package androidx.health.connect.client.impl.platform.records
+
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
+import androidx.health.connect.client.records.BasalBodyTemperatureRecord
+import androidx.health.connect.client.records.BasalMetabolicRateRecord
+import androidx.health.connect.client.records.BloodGlucoseRecord
+import androidx.health.connect.client.records.BloodPressureRecord
+import androidx.health.connect.client.records.BodyFatRecord
+import androidx.health.connect.client.records.BodyTemperatureRecord
+import androidx.health.connect.client.records.BodyWaterMassRecord
+import androidx.health.connect.client.records.BoneMassRecord
+import androidx.health.connect.client.records.CervicalMucusRecord
+import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
+import androidx.health.connect.client.records.DistanceRecord
+import androidx.health.connect.client.records.ElevationGainedRecord
+import androidx.health.connect.client.records.ExerciseSessionRecord
+import androidx.health.connect.client.records.FloorsClimbedRecord
+import androidx.health.connect.client.records.HeartRateRecord
+import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
+import androidx.health.connect.client.records.HeightRecord
+import androidx.health.connect.client.records.HydrationRecord
+import androidx.health.connect.client.records.IntermenstrualBleedingRecord
+import androidx.health.connect.client.records.LeanBodyMassRecord
+import androidx.health.connect.client.records.MenstruationFlowRecord
+import androidx.health.connect.client.records.MenstruationPeriodRecord
+import androidx.health.connect.client.records.NutritionRecord
+import androidx.health.connect.client.records.OvulationTestRecord
+import androidx.health.connect.client.records.OxygenSaturationRecord
+import androidx.health.connect.client.records.PowerRecord
+import androidx.health.connect.client.records.Record
+import androidx.health.connect.client.records.RespiratoryRateRecord
+import androidx.health.connect.client.records.RestingHeartRateRecord
+import androidx.health.connect.client.records.SexualActivityRecord
+import androidx.health.connect.client.records.SleepSessionRecord
+import androidx.health.connect.client.records.SpeedRecord
+import androidx.health.connect.client.records.StepsCadenceRecord
+import androidx.health.connect.client.records.StepsRecord
+import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
+import androidx.health.connect.client.records.Vo2MaxRecord
+import androidx.health.connect.client.records.WeightRecord
+import androidx.health.connect.client.records.WheelchairPushesRecord
+import kotlin.reflect.KClass
+
+// TODO(b/270559291): Validate that all class fields are being converted.
+
+fun KClass<out Record>.toPlatformRecordClass(): Class<out PlatformRecord> {
+ return SDK_TO_PLATFORM_RECORD_CLASS[this]
+ ?: throw IllegalArgumentException("Unsupported record type $this")
+}
+
+fun Record.toPlatformRecord(): PlatformRecord {
+ return when (this) {
+ is ActiveCaloriesBurnedRecord -> toPlatformActiveCaloriesBurnedRecord()
+ is BasalBodyTemperatureRecord -> toPlatformBasalBodyTemperatureRecord()
+ is BasalMetabolicRateRecord -> toPlatformBasalMetabolicRateRecord()
+ is BloodGlucoseRecord -> toPlatformBloodGlucoseRecord()
+ is BloodPressureRecord -> toPlatformBloodPressureRecord()
+ is BodyFatRecord -> toPlatformBodyFatRecord()
+ is BodyTemperatureRecord -> toPlatformBodyTemperatureRecord()
+ is BodyWaterMassRecord -> toPlatformBodyWaterMassRecord()
+ is BoneMassRecord -> toPlatformBoneMassRecord()
+ is CervicalMucusRecord -> toPlatformCervicalMucusRecord()
+ is CyclingPedalingCadenceRecord -> toPlatformCyclingPedalingCadenceRecord()
+ is DistanceRecord -> toPlatformDistanceRecord()
+ is ElevationGainedRecord -> toPlatformElevationGainedRecord()
+ is ExerciseSessionRecord -> toPlatformExerciseSessionRecord()
+ is FloorsClimbedRecord -> toPlatformFloorsClimbedRecord()
+ is HeartRateRecord -> toPlatformHeartRateRecord()
+ is HeartRateVariabilityRmssdRecord -> toPlatformHeartRateVariabilityRmssdRecord()
+ is HeightRecord -> toPlatformHeightRecord()
+ is HydrationRecord -> toPlatformHydrationRecord()
+ is IntermenstrualBleedingRecord -> toPlatformIntermenstrualBleedingRecord()
+ is LeanBodyMassRecord -> toPlatformLeanBodyMassRecord()
+ is MenstruationFlowRecord -> toPlatformMenstruationFlowRecord()
+ is MenstruationPeriodRecord -> toPlatformMenstruationPeriodRecord()
+ is NutritionRecord -> toPlatformNutritionRecord()
+ is OvulationTestRecord -> toPlatformOvulationTestRecord()
+ is OxygenSaturationRecord -> toPlatformOxygenSaturationRecord()
+ is PowerRecord -> toPlatformPowerRecord()
+ is RespiratoryRateRecord -> toPlatformRespiratoryRateRecord()
+ is RestingHeartRateRecord -> toPlatformRestingHeartRateRecord()
+ is SexualActivityRecord -> toPlatformSexualActivityRecord()
+ is SleepSessionRecord -> toPlatformSleepSessionRecord()
+ is SpeedRecord -> toPlatformSpeedRecord()
+ is StepsCadenceRecord -> toPlatformStepsCadenceRecord()
+ is StepsRecord -> toPlatformStepsRecord()
+ is TotalCaloriesBurnedRecord -> toPlatformTotalCaloriesBurnedRecord()
+ is Vo2MaxRecord -> toPlatformVo2MaxRecord()
+ is WeightRecord -> toPlatformWeightRecord()
+ is WheelchairPushesRecord -> toPlatformWheelchairPushesRecord()
+ else -> throw IllegalArgumentException("Unsupported record $this")
+ }
+}
+
+fun PlatformRecord.toSdkRecord(): Record {
+ return when (this) {
+ is PlatformActiveCaloriesBurnedRecord -> toSdkActiveCaloriesBurnedRecord()
+ is PlatformBasalBodyTemperatureRecord -> toSdkBasalBodyTemperatureRecord()
+ is PlatformBasalMetabolicRateRecord -> toSdkBasalMetabolicRateRecord()
+ is PlatformBloodGlucoseRecord -> toSdkBloodGlucoseRecord()
+ is PlatformBloodPressureRecord -> toSdkBloodPressureRecord()
+ is PlatformBodyFatRecord -> toSdkBodyFatRecord()
+ is PlatformBodyTemperatureRecord -> toSdkBodyTemperatureRecord()
+ is PlatformBodyWaterMassRecord -> toSdkBodyWaterMassRecord()
+ is PlatformBoneMassRecord -> toSdkBoneMassRecord()
+ is PlatformCervicalMucusRecord -> toSdkCervicalMucusRecord()
+ is PlatformCyclingPedalingCadenceRecord -> toSdkCyclingPedalingCadenceRecord()
+ is PlatformDistanceRecord -> toSdkDistanceRecord()
+ is PlatformElevationGainedRecord -> toSdkElevationGainedRecord()
+ is PlatformExerciseSessionRecord -> toSdkExerciseSessionRecord()
+ is PlatformFloorsClimbedRecord -> toSdkFloorsClimbedRecord()
+ is PlatformHeartRateRecord -> toSdkHeartRateRecord()
+ is PlatformHeartRateVariabilityRmssdRecord -> toSdkHeartRateVariabilityRmssdRecord()
+ is PlatformHeightRecord -> toSdkHeightRecord()
+ is PlatformHydrationRecord -> toSdkHydrationRecord()
+ is PlatformIntermenstrualBleedingRecord -> toSdkIntermenstrualBleedingRecord()
+ is PlatformLeanBodyMassRecord -> toSdkLeanBodyMassRecord()
+ is PlatformMenstruationFlowRecord -> toSdkMenstruationFlowRecord()
+ is PlatformMenstruationPeriodRecord -> toSdkMenstruationPeriodRecord()
+ is PlatformNutritionRecord -> toSdkNutritionRecord()
+ is PlatformOvulationTestRecord -> toSdkOvulationTestRecord()
+ is PlatformOxygenSaturationRecord -> toSdkOxygenSaturationRecord()
+ is PlatformPowerRecord -> toSdkPowerRecord()
+ is PlatformRespiratoryRateRecord -> toSdkRespiratoryRateRecord()
+ is PlatformRestingHeartRateRecord -> toSdkRestingHeartRateRecord()
+ is PlatformSexualActivityRecord -> toSdkSexualActivityRecord()
+ is PlatformSleepSessionRecord -> toSdkSleepSessionRecord()
+ is PlatformSpeedRecord -> toSdkSpeedRecord()
+ is PlatformStepsCadenceRecord -> toSdkStepsCadenceRecord()
+ is PlatformStepsRecord -> toSdkStepsRecord()
+ is PlatformTotalCaloriesBurnedRecord -> toSdkTotalCaloriesBurnedRecord()
+ is PlatformVo2MaxRecord -> toSdkVo2MaxRecord()
+ is PlatformWeightRecord -> toSdkWeightRecord()
+ is PlatformWheelchairPushesRecord -> toWheelchairPushesRecord()
+ else -> throw IllegalArgumentException("Unsupported record $this")
+ }
+}
+
+private fun PlatformActiveCaloriesBurnedRecord.toSdkActiveCaloriesBurnedRecord() =
+ ActiveCaloriesBurnedRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ energy = energy.toSdkEnergy(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformBasalBodyTemperatureRecord.toSdkBasalBodyTemperatureRecord() =
+ BasalBodyTemperatureRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ temperature = temperature.toSdkTemperature(),
+ measurementLocation = measurementLocation,
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformBasalMetabolicRateRecord.toSdkBasalMetabolicRateRecord() =
+ BasalMetabolicRateRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ basalMetabolicRate = basalMetabolicRate.toSdkPower(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformBloodGlucoseRecord.toSdkBloodGlucoseRecord() =
+ BloodGlucoseRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ level = level.toSdkBloodGlucose(),
+ specimenSource = specimenSource.toSdkBloodGlucoseSpecimenSource(),
+ mealType = mealType.toSdkMealType(),
+ relationToMeal = relationToMeal.toSdkRelationToMeal(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformBloodPressureRecord.toSdkBloodPressureRecord() =
+ BloodPressureRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ systolic = systolic.toSdkPressure(),
+ diastolic = diastolic.toSdkPressure(),
+ bodyPosition = bodyPosition.toSdkBloodPressureBodyPosition(),
+ measurementLocation = measurementLocation.toSdkBloodPressureMeasurementLocation(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformBodyFatRecord.toSdkBodyFatRecord() =
+ BodyFatRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ percentage = percentage.toSdkPercentage(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformBodyTemperatureRecord.toSdkBodyTemperatureRecord() =
+ BodyTemperatureRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ temperature = temperature.toSdkTemperature(),
+ measurementLocation = measurementLocation.toSdkBodyTemperatureMeasurementLocation(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformBodyWaterMassRecord.toSdkBodyWaterMassRecord() =
+ BodyWaterMassRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ mass = bodyWaterMass.toSdkMass(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformBoneMassRecord.toSdkBoneMassRecord() =
+ BoneMassRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ mass = mass.toSdkMass(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformCervicalMucusRecord.toSdkCervicalMucusRecord() =
+ CervicalMucusRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ appearance = appearance.toSdkCervicalMucusAppearance(),
+ sensation = sensation.toSdkCervicalMucusSensation(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformCyclingPedalingCadenceRecord.toSdkCyclingPedalingCadenceRecord() =
+ CyclingPedalingCadenceRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ samples = samples.map { it.toSdkCyclingPedalingCadenceSample() },
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformDistanceRecord.toSdkDistanceRecord() =
+ DistanceRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ distance = distance.toSdkLength(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformElevationGainedRecord.toSdkElevationGainedRecord() =
+ ElevationGainedRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ elevation = elevation.toSdkLength(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformExerciseSessionRecord.toSdkExerciseSessionRecord() =
+ ExerciseSessionRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ exerciseType = exerciseType.toSdkExerciseSessionType(),
+ title = title?.toString(),
+ notes = notes?.toString(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformFloorsClimbedRecord.toSdkFloorsClimbedRecord() =
+ FloorsClimbedRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ floors = floors,
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformHeartRateRecord.toSdkHeartRateRecord() =
+ HeartRateRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ samples = samples.map { it.toSdkHeartRateSample() },
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformHeartRateVariabilityRmssdRecord.toSdkHeartRateVariabilityRmssdRecord() =
+ HeartRateVariabilityRmssdRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ heartRateVariabilityMillis = heartRateVariabilityMillis,
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformHeightRecord.toSdkHeightRecord() =
+ HeightRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ height = height.toSdkLength(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformHydrationRecord.toSdkHydrationRecord() =
+ HydrationRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ volume = volume.toSdkVolume(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformIntermenstrualBleedingRecord.toSdkIntermenstrualBleedingRecord() =
+ IntermenstrualBleedingRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformLeanBodyMassRecord.toSdkLeanBodyMassRecord() =
+ LeanBodyMassRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ mass = mass.toSdkMass(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformMenstruationFlowRecord.toSdkMenstruationFlowRecord() =
+ MenstruationFlowRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ flow = flow.toSdkMenstruationFlow(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformMenstruationPeriodRecord.toSdkMenstruationPeriodRecord() =
+ MenstruationPeriodRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformNutritionRecord.toSdkNutritionRecord() =
+ NutritionRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ name = mealName,
+ mealType = mealType.toSdkMealType(),
+ metadata = metadata.toSdkMetadata(),
+ biotin = biotin?.toSdkMass(),
+ caffeine = caffeine?.toSdkMass(),
+ calcium = calcium?.toSdkMass(),
+ energy = energy?.toSdkEnergy(),
+ energyFromFat = energyFromFat?.toSdkEnergy(),
+ chloride = chloride?.toSdkMass(),
+ cholesterol = cholesterol?.toSdkMass(),
+ chromium = chromium?.toSdkMass(),
+ copper = copper?.toSdkMass(),
+ dietaryFiber = dietaryFiber?.toSdkMass(),
+ folate = folate?.toSdkMass(),
+ folicAcid = folicAcid?.toSdkMass(),
+ iodine = iodine?.toSdkMass(),
+ iron = iron?.toSdkMass(),
+ magnesium = magnesium?.toSdkMass(),
+ manganese = manganese?.toSdkMass(),
+ molybdenum = molybdenum?.toSdkMass(),
+ monounsaturatedFat = monounsaturatedFat?.toSdkMass(),
+ niacin = niacin?.toSdkMass(),
+ pantothenicAcid = pantothenicAcid?.toSdkMass(),
+ phosphorus = phosphorus?.toSdkMass(),
+ polyunsaturatedFat = polyunsaturatedFat?.toSdkMass(),
+ potassium = potassium?.toSdkMass(),
+ protein = protein?.toSdkMass(),
+ riboflavin = riboflavin?.toSdkMass(),
+ saturatedFat = saturatedFat?.toSdkMass(),
+ selenium = selenium?.toSdkMass(),
+ sodium = sodium?.toSdkMass(),
+ sugar = sugar?.toSdkMass(),
+ thiamin = thiamin?.toSdkMass(),
+ totalCarbohydrate = totalCarbohydrate?.toSdkMass(),
+ totalFat = totalFat?.toSdkMass(),
+ transFat = transFat?.toSdkMass(),
+ unsaturatedFat = unsaturatedFat?.toSdkMass(),
+ vitaminA = vitaminA?.toSdkMass(),
+ vitaminB12 = vitaminB12?.toSdkMass(),
+ vitaminB6 = vitaminB6?.toSdkMass(),
+ vitaminC = vitaminC?.toSdkMass(),
+ vitaminD = vitaminD?.toSdkMass(),
+ vitaminE = vitaminE?.toSdkMass(),
+ vitaminK = vitaminK?.toSdkMass(),
+ zinc = zinc?.toSdkMass()
+ )
+
+private fun PlatformOvulationTestRecord.toSdkOvulationTestRecord() =
+ OvulationTestRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ result = result.toSdkOvulationTestResult(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformOxygenSaturationRecord.toSdkOxygenSaturationRecord() =
+ OxygenSaturationRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ percentage = percentage.toSdkPercentage(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformPowerRecord.toSdkPowerRecord() =
+ PowerRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ samples = samples.map { it.toSdkPowerRecordSample() },
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformRespiratoryRateRecord.toSdkRespiratoryRateRecord() =
+ RespiratoryRateRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ rate = rate,
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformRestingHeartRateRecord.toSdkRestingHeartRateRecord() =
+ RestingHeartRateRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ beatsPerMinute = beatsPerMinute,
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformSexualActivityRecord.toSdkSexualActivityRecord() =
+ SexualActivityRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ protectionUsed = protectionUsed.toSdkProtectionUsed(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformSleepSessionRecord.toSdkSleepSessionRecord() =
+ SleepSessionRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ metadata = metadata.toSdkMetadata(),
+ title = title?.toString(),
+ notes = notes?.toString()
+ )
+
+private fun PlatformSpeedRecord.toSdkSpeedRecord() =
+ SpeedRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ samples = samples.map { it.toSdkSpeedSample() },
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformStepsCadenceRecord.toSdkStepsCadenceRecord() =
+ StepsCadenceRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ samples = samples.map { it.toSdkStepsCadenceSample() },
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformStepsRecord.toSdkStepsRecord() =
+ StepsRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ count = count,
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformTotalCaloriesBurnedRecord.toSdkTotalCaloriesBurnedRecord() =
+ TotalCaloriesBurnedRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ energy = energy.toSdkEnergy(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformVo2MaxRecord.toSdkVo2MaxRecord() =
+ Vo2MaxRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ vo2MillilitersPerMinuteKilogram = vo2MillilitersPerMinuteKilogram,
+ measurementMethod = measurementMethod.toSdkVo2MaxMeasurementMethod(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformWeightRecord.toSdkWeightRecord() =
+ WeightRecord(
+ time = time,
+ zoneOffset = zoneOffset,
+ weight = weight.toSdkMass(),
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun PlatformWheelchairPushesRecord.toWheelchairPushesRecord() =
+ WheelchairPushesRecord(
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ count = count,
+ metadata = metadata.toSdkMetadata()
+ )
+
+private fun ActiveCaloriesBurnedRecord.toPlatformActiveCaloriesBurnedRecord() =
+ PlatformActiveCaloriesBurnedRecordBuilder(
+ metadata.toPlatformMetadata(),
+ startTime,
+ endTime,
+ energy.toPlatformEnergy(),
+ )
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ }
+ .build()
+
+private fun BasalBodyTemperatureRecord.toPlatformBasalBodyTemperatureRecord() =
+ PlatformBasalBodyTemperatureRecordBuilder(
+ metadata.toPlatformMetadata(),
+ time,
+ measurementLocation.toPlatformBodyTemperatureMeasurementLocation(),
+ temperature.toPlatformTemperature()
+ )
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun BasalMetabolicRateRecord.toPlatformBasalMetabolicRateRecord() =
+ PlatformBasalMetabolicRateRecordBuilder(
+ metadata.toPlatformMetadata(),
+ time,
+ basalMetabolicRate.toPlatformPower()
+ )
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun BloodGlucoseRecord.toPlatformBloodGlucoseRecord() =
+ PlatformBloodGlucoseRecordBuilder(
+ metadata.toPlatformMetadata(),
+ time,
+ specimenSource.toPlatformBloodGlucoseSpecimenSource(),
+ level.toPlatformBloodGlucose(),
+ relationToMeal.toPlatformBloodGlucoseRelationToMeal(),
+ mealType.toPlatformMealType()
+ )
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun BloodPressureRecord.toPlatformBloodPressureRecord() =
+ PlatformBloodPressureRecordBuilder(
+ metadata.toPlatformMetadata(),
+ time,
+ measurementLocation.toPlatformBloodPressureMeasurementLocation(),
+ systolic.toPlatformPressure(),
+ diastolic.toPlatformPressure(),
+ bodyPosition.toPlatformBloodPressureBodyPosition()
+ )
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun BodyFatRecord.toPlatformBodyFatRecord() =
+ PlatformBodyFatRecordBuilder(
+ metadata.toPlatformMetadata(),
+ time,
+ percentage.toPlatformPercentage()
+ )
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun BodyTemperatureRecord.toPlatformBodyTemperatureRecord() =
+ PlatformBodyTemperatureRecordBuilder(
+ metadata.toPlatformMetadata(),
+ time,
+ measurementLocation.toPlatformBodyTemperatureMeasurementLocation(),
+ temperature.toPlatformTemperature()
+ )
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun BodyWaterMassRecord.toPlatformBodyWaterMassRecord() =
+ PlatformBodyWaterMassRecordBuilder(metadata.toPlatformMetadata(), time, mass.toPlatformMass())
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun BoneMassRecord.toPlatformBoneMassRecord() =
+ PlatformBoneMassRecordBuilder(metadata.toPlatformMetadata(), time, mass.toPlatformMass())
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun CervicalMucusRecord.toPlatformCervicalMucusRecord() =
+ PlatformCervicalMucusRecordBuilder(
+ metadata.toPlatformMetadata(),
+ time,
+ sensation.toPlatformCervicalMucusSensation(),
+ appearance.toPlatformCervicalMucusAppearance(),
+ )
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun CyclingPedalingCadenceRecord.toPlatformCyclingPedalingCadenceRecord() =
+ PlatformCyclingPedalingCadenceRecordBuilder(
+ metadata.toPlatformMetadata(),
+ startTime,
+ endTime,
+ samples.map { it.toPlatformCyclingPedalingCadenceSample() }
+ )
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ }
+ .build()
+
+private fun CyclingPedalingCadenceRecord.Sample.toPlatformCyclingPedalingCadenceSample() =
+ PlatformCyclingPedalingCadenceSample(revolutionsPerMinute, time)
+
+private fun DistanceRecord.toPlatformDistanceRecord() =
+ PlatformDistanceRecordBuilder(
+ metadata.toPlatformMetadata(),
+ startTime,
+ endTime,
+ distance.toPlatformLength()
+ )
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ }
+ .build()
+
+private fun ElevationGainedRecord.toPlatformElevationGainedRecord() =
+ PlatformElevationGainedRecordBuilder(
+ metadata.toPlatformMetadata(),
+ startTime,
+ endTime,
+ elevation.toPlatformLength()
+ )
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ }
+ .build()
+
+private fun ExerciseSessionRecord.toPlatformExerciseSessionRecord() =
+ PlatformExerciseSessionRecordBuilder(
+ metadata.toPlatformMetadata(),
+ startTime,
+ endTime,
+ exerciseType.toPlatformExerciseSessionType()
+ )
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ notes?.let { setNotes(it) }
+ title?.let { setTitle(it) }
+ }
+ .build()
+
+private fun FloorsClimbedRecord.toPlatformFloorsClimbedRecord() =
+ PlatformFloorsClimbedRecordBuilder(metadata.toPlatformMetadata(), startTime, endTime, floors)
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ }
+ .build()
+
+private fun HeartRateRecord.toPlatformHeartRateRecord() =
+ PlatformHeartRateRecordBuilder(
+ metadata.toPlatformMetadata(),
+ startTime,
+ endTime,
+ samples.map { it.toPlatformHeartRateSample() }
+ )
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ }
+ .build()
+
+private fun HeartRateRecord.Sample.toPlatformHeartRateSample() =
+ PlatformHeartRateSample(beatsPerMinute, time)
+
+private fun HeartRateVariabilityRmssdRecord.toPlatformHeartRateVariabilityRmssdRecord() =
+ PlatformHeartRateVariabilityRmssdRecordBuilder(
+ metadata.toPlatformMetadata(),
+ time,
+ heartRateVariabilityMillis
+ )
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun HeightRecord.toPlatformHeightRecord() =
+ PlatformHeightRecordBuilder(metadata.toPlatformMetadata(), time, height.toPlatformLength())
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun HydrationRecord.toPlatformHydrationRecord() =
+ PlatformHydrationRecordBuilder(
+ metadata.toPlatformMetadata(),
+ startTime,
+ endTime,
+ volume.toPlatformVolume()
+ )
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ }
+ .build()
+
+private fun IntermenstrualBleedingRecord.toPlatformIntermenstrualBleedingRecord() =
+ PlatformIntermenstrualBleedingRecordBuilder(metadata.toPlatformMetadata(), time)
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun LeanBodyMassRecord.toPlatformLeanBodyMassRecord() =
+ PlatformLeanBodyMassRecordBuilder(metadata.toPlatformMetadata(), time, mass.toPlatformMass())
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun MenstruationFlowRecord.toPlatformMenstruationFlowRecord() =
+ PlatformMenstruationFlowRecordBuilder(
+ metadata.toPlatformMetadata(),
+ time,
+ flow.toPlatformMenstruationFlow()
+ )
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun MenstruationPeriodRecord.toPlatformMenstruationPeriodRecord() =
+ PlatformMenstruationPeriodRecordBuilder(metadata.toPlatformMetadata(), startTime, endTime)
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ }
+ .build()
+
+private fun NutritionRecord.toPlatformNutritionRecord() =
+ PlatformNutritionRecordBuilder(metadata.toPlatformMetadata(), startTime, endTime)
+ .setMealType(mealType.toPlatformMealType())
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ biotin?.let { setBiotin(it.toPlatformMass()) }
+ calcium?.let { setCalcium(it.toPlatformMass()) }
+ caffeine?.let { setCaffeine(it.toPlatformMass()) }
+ dietaryFiber?.let { setDietaryFiber(it.toPlatformMass()) }
+ energy?.let { setEnergy(it.toPlatformEnergy()) }
+ energyFromFat?.let { setEnergyFromFat(it.toPlatformEnergy()) }
+ folate?.let { setFolate(it.toPlatformMass()) }
+ folicAcid?.let { setFolicAcid(it.toPlatformMass()) }
+ iodine?.let { setIodine(it.toPlatformMass()) }
+ iron?.let { setIron(it.toPlatformMass()) }
+ magnesium?.let { setMagnesium(it.toPlatformMass()) }
+ manganese?.let { setManganese(it.toPlatformMass()) }
+ name?.let { setMealName(it) }
+ niacin?.let { setNiacin(it.toPlatformMass()) }
+ pantothenicAcid?.let { setPantothenicAcid(it.toPlatformMass()) }
+ phosphorus?.let { setPhosphorus(it.toPlatformMass()) }
+ polyunsaturatedFat?.let { setPolyunsaturatedFat(it.toPlatformMass()) }
+ potassium?.let { setPotassium(it.toPlatformMass()) }
+ protein?.let { setProtein(it.toPlatformMass()) }
+ riboflavin?.let { setRiboflavin(it.toPlatformMass()) }
+ saturatedFat?.let { setSaturatedFat(it.toPlatformMass()) }
+ selenium?.let { setSelenium(it.toPlatformMass()) }
+ sodium?.let { setSodium(it.toPlatformMass()) }
+ sugar?.let { setSugar(it.toPlatformMass()) }
+ thiamin?.let { setThiamin(it.toPlatformMass()) }
+ totalCarbohydrate?.let { setTotalCarbohydrate(it.toPlatformMass()) }
+ totalFat?.let { setTotalFat(it.toPlatformMass()) }
+ transFat?.let { setTransFat(it.toPlatformMass()) }
+ unsaturatedFat?.let { setUnsaturatedFat(it.toPlatformMass()) }
+ vitaminA?.let { setVitaminA(it.toPlatformMass()) }
+ vitaminB6?.let { setVitaminB6(it.toPlatformMass()) }
+ vitaminB12?.let { setVitaminB12(it.toPlatformMass()) }
+ vitaminC?.let { setVitaminC(it.toPlatformMass()) }
+ vitaminD?.let { setVitaminD(it.toPlatformMass()) }
+ vitaminE?.let { setVitaminE(it.toPlatformMass()) }
+ vitaminK?.let { setVitaminK(it.toPlatformMass()) }
+ zinc?.let { setZinc(it.toPlatformMass()) }
+ }
+ .build()
+
+private fun OvulationTestRecord.toPlatformOvulationTestRecord() =
+ PlatformOvulationTestRecordBuilder(
+ metadata.toPlatformMetadata(),
+ time,
+ result.toPlatformOvulationTestResult()
+ )
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun OxygenSaturationRecord.toPlatformOxygenSaturationRecord() =
+ PlatformOxygenSaturationRecordBuilder(
+ metadata.toPlatformMetadata(),
+ time,
+ percentage.toPlatformPercentage()
+ )
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun PowerRecord.toPlatformPowerRecord() =
+ PlatformPowerRecordBuilder(
+ metadata.toPlatformMetadata(),
+ startTime,
+ endTime,
+ samples.map { it.toPlatformPowerRecordSample() }
+ )
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ }
+ .build()
+
+private fun PowerRecord.Sample.toPlatformPowerRecordSample() =
+ PlatformPowerRecordSample(power.toPlatformPower(), time)
+
+private fun RespiratoryRateRecord.toPlatformRespiratoryRateRecord() =
+ PlatformRespiratoryRateRecordBuilder(metadata.toPlatformMetadata(), time, rate)
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun RestingHeartRateRecord.toPlatformRestingHeartRateRecord() =
+ PlatformRestingHeartRateRecordBuilder(metadata.toPlatformMetadata(), time, beatsPerMinute)
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun SexualActivityRecord.toPlatformSexualActivityRecord() =
+ PlatformSexualActivityRecordBuilder(
+ metadata.toPlatformMetadata(),
+ time,
+ protectionUsed.toPlatformSexualActivityProtectionUsed()
+ )
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun SleepSessionRecord.toPlatformSleepSessionRecord() =
+ PlatformSleepSessionRecordBuilder(metadata.toPlatformMetadata(), startTime, endTime)
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ notes?.let { setNotes(it) }
+ title?.let { setTitle(it) }
+ }
+ .build()
+
+private fun SpeedRecord.toPlatformSpeedRecord() =
+ PlatformSpeedRecordBuilder(
+ metadata.toPlatformMetadata(),
+ startTime,
+ endTime,
+ samples.map { it.toPlatformSpeedRecordSample() }
+ )
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ }
+ .build()
+
+private fun SpeedRecord.Sample.toPlatformSpeedRecordSample() =
+ PlatformSpeedSample(speed.toPlatformVelocity(), time)
+
+private fun StepsRecord.toPlatformStepsRecord() =
+ PlatformStepsRecordBuilder(metadata.toPlatformMetadata(), startTime, endTime, count)
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ }
+ .build()
+
+private fun StepsCadenceRecord.toPlatformStepsCadenceRecord() =
+ PlatformStepsCadenceRecordBuilder(
+ metadata.toPlatformMetadata(),
+ startTime,
+ endTime,
+ samples.map { it.toPlatformStepsCadenceSample() }
+ )
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ }
+ .build()
+
+private fun StepsCadenceRecord.Sample.toPlatformStepsCadenceSample() =
+ PlatformStepsCadenceSample(rate, time)
+
+private fun TotalCaloriesBurnedRecord.toPlatformTotalCaloriesBurnedRecord() =
+ PlatformTotalCaloriesBurnedRecordBuilder(
+ metadata.toPlatformMetadata(),
+ startTime,
+ endTime,
+ energy.toPlatformEnergy()
+ )
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ }
+ .build()
+
+private fun Vo2MaxRecord.toPlatformVo2MaxRecord() =
+ PlatformVo2MaxRecordBuilder(
+ metadata.toPlatformMetadata(),
+ time,
+ measurementMethod.toPlatformVo2MaxMeasurementMethod(),
+ vo2MillilitersPerMinuteKilogram
+ )
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun WeightRecord.toPlatformWeightRecord() =
+ PlatformWeightRecordBuilder(metadata.toPlatformMetadata(), time, weight.toPlatformMass())
+ .apply { zoneOffset?.let { setZoneOffset(it) } }
+ .build()
+
+private fun WheelchairPushesRecord.toPlatformWheelchairPushesRecord() =
+ PlatformWheelchairPushesRecordBuilder(metadata.toPlatformMetadata(), startTime, endTime, count)
+ .apply {
+ startZoneOffset?.let { setStartZoneOffset(it) }
+ endZoneOffset?.let { setEndZoneOffset(it) }
+ }
+ .build()
+
+private fun PlatformCyclingPedalingCadenceSample.toSdkCyclingPedalingCadenceSample() =
+ CyclingPedalingCadenceRecord.Sample(time, revolutionsPerMinute)
+
+private fun PlatformHeartRateSample.toSdkHeartRateSample() =
+ HeartRateRecord.Sample(time, beatsPerMinute)
+
+private fun PlatformPowerRecordSample.toSdkPowerRecordSample() =
+ PowerRecord.Sample(time, power.toSdkPower())
+
+private fun PlatformSpeedSample.toSdkSpeedSample() = SpeedRecord.Sample(time, speed.toSdkVelocity())
+
+private fun PlatformStepsCadenceSample.toSdkStepsCadenceSample() =
+ StepsCadenceRecord.Sample(time, rate)
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RecordMappings.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RecordMappings.kt
new file mode 100644
index 0000000..46d32a4
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RecordMappings.kt
@@ -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.
+ */
+
+@file:RestrictTo(RestrictTo.Scope.LIBRARY)
+@file:RequiresApi(api = 34)
+
+package androidx.health.connect.client.impl.platform.records
+
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
+import androidx.health.connect.client.records.BasalBodyTemperatureRecord
+import androidx.health.connect.client.records.BasalMetabolicRateRecord
+import androidx.health.connect.client.records.BloodGlucoseRecord
+import androidx.health.connect.client.records.BloodPressureRecord
+import androidx.health.connect.client.records.BodyFatRecord
+import androidx.health.connect.client.records.BodyTemperatureRecord
+import androidx.health.connect.client.records.BodyWaterMassRecord
+import androidx.health.connect.client.records.BoneMassRecord
+import androidx.health.connect.client.records.CervicalMucusRecord
+import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
+import androidx.health.connect.client.records.DistanceRecord
+import androidx.health.connect.client.records.ElevationGainedRecord
+import androidx.health.connect.client.records.ExerciseSessionRecord
+import androidx.health.connect.client.records.FloorsClimbedRecord
+import androidx.health.connect.client.records.HeartRateRecord
+import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
+import androidx.health.connect.client.records.HeightRecord
+import androidx.health.connect.client.records.HydrationRecord
+import androidx.health.connect.client.records.IntermenstrualBleedingRecord
+import androidx.health.connect.client.records.LeanBodyMassRecord
+import androidx.health.connect.client.records.MenstruationFlowRecord
+import androidx.health.connect.client.records.MenstruationPeriodRecord
+import androidx.health.connect.client.records.NutritionRecord
+import androidx.health.connect.client.records.OvulationTestRecord
+import androidx.health.connect.client.records.OxygenSaturationRecord
+import androidx.health.connect.client.records.PowerRecord
+import androidx.health.connect.client.records.Record
+import androidx.health.connect.client.records.RespiratoryRateRecord
+import androidx.health.connect.client.records.RestingHeartRateRecord
+import androidx.health.connect.client.records.SexualActivityRecord
+import androidx.health.connect.client.records.SleepSessionRecord
+import androidx.health.connect.client.records.SpeedRecord
+import androidx.health.connect.client.records.StepsCadenceRecord
+import androidx.health.connect.client.records.StepsRecord
+import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
+import androidx.health.connect.client.records.Vo2MaxRecord
+import androidx.health.connect.client.records.WeightRecord
+import androidx.health.connect.client.records.WheelchairPushesRecord
+import kotlin.reflect.KClass
+
+internal val SDK_TO_PLATFORM_RECORD_CLASS: Map<KClass<out Record>, Class<out PlatformRecord>> =
+ mapOf(
+ ActiveCaloriesBurnedRecord::class to PlatformActiveCaloriesBurnedRecord::class.java,
+ BasalBodyTemperatureRecord::class to PlatformBasalBodyTemperatureRecord::class.java,
+ BasalMetabolicRateRecord::class to PlatformBasalMetabolicRateRecord::class.java,
+ BloodGlucoseRecord::class to PlatformBloodGlucoseRecord::class.java,
+ BloodPressureRecord::class to PlatformBloodPressureRecord::class.java,
+ BodyFatRecord::class to PlatformBodyFatRecord::class.java,
+ BodyTemperatureRecord::class to PlatformBodyTemperatureRecord::class.java,
+ BodyWaterMassRecord::class to PlatformBodyWaterMassRecord::class.java,
+ BoneMassRecord::class to PlatformBoneMassRecord::class.java,
+ CervicalMucusRecord::class to PlatformCervicalMucusRecord::class.java,
+ CyclingPedalingCadenceRecord::class to PlatformCyclingPedalingCadenceRecord::class.java,
+ DistanceRecord::class to PlatformDistanceRecord::class.java,
+ ElevationGainedRecord::class to PlatformElevationGainedRecord::class.java,
+ ExerciseSessionRecord::class to PlatformExerciseSessionRecord::class.java,
+ FloorsClimbedRecord::class to PlatformFloorsClimbedRecord::class.java,
+ HeartRateRecord::class to PlatformHeartRateRecord::class.java,
+ HeartRateVariabilityRmssdRecord::class to
+ PlatformHeartRateVariabilityRmssdRecord::class.java,
+ HeightRecord::class to PlatformHeightRecord::class.java,
+ HydrationRecord::class to PlatformHydrationRecord::class.java,
+ IntermenstrualBleedingRecord::class to PlatformIntermenstrualBleedingRecord::class.java,
+ LeanBodyMassRecord::class to PlatformLeanBodyMassRecord::class.java,
+ MenstruationFlowRecord::class to PlatformMenstruationFlowRecord::class.java,
+ MenstruationPeriodRecord::class to PlatformMenstruationPeriodRecord::class.java,
+ NutritionRecord::class to PlatformNutritionRecord::class.java,
+ OvulationTestRecord::class to PlatformOvulationTestRecord::class.java,
+ OxygenSaturationRecord::class to PlatformOxygenSaturationRecord::class.java,
+ PowerRecord::class to PlatformPowerRecord::class.java,
+ RespiratoryRateRecord::class to PlatformRespiratoryRateRecord::class.java,
+ RestingHeartRateRecord::class to PlatformRestingHeartRateRecord::class.java,
+ SexualActivityRecord::class to PlatformSexualActivityRecord::class.java,
+ SleepSessionRecord::class to PlatformSleepSessionRecord::class.java,
+ SpeedRecord::class to PlatformSpeedRecord::class.java,
+ StepsCadenceRecord::class to PlatformStepsCadenceRecord::class.java,
+ StepsRecord::class to PlatformStepsRecord::class.java,
+ TotalCaloriesBurnedRecord::class to PlatformTotalCaloriesBurnedRecord::class.java,
+ Vo2MaxRecord::class to PlatformVo2MaxRecord::class.java,
+ WeightRecord::class to PlatformWeightRecord::class.java,
+ WheelchairPushesRecord::class to PlatformWheelchairPushesRecord::class.java,
+ )
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RequestConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RequestConverters.kt
new file mode 100644
index 0000000..b000546
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RequestConverters.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:RestrictTo(RestrictTo.Scope.LIBRARY)
+@file:RequiresApi(api = 34)
+
+package androidx.health.connect.client.impl.platform.records
+
+import android.health.connect.AggregateRecordsRequest
+import android.health.connect.LocalTimeRangeFilter
+import android.health.connect.ReadRecordsRequestUsingFilters
+import android.health.connect.TimeInstantRangeFilter
+import android.health.connect.TimeRangeFilter as PlatformTimeRangeFilter
+import android.health.connect.changelog.ChangeLogTokenRequest
+import android.health.connect.datatypes.AggregationType
+import android.health.connect.datatypes.Record as PlatformRecord
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.aggregate.AggregateMetric
+import androidx.health.connect.client.impl.platform.time.TimeSource
+import androidx.health.connect.client.records.Record
+import androidx.health.connect.client.request.AggregateGroupByDurationRequest
+import androidx.health.connect.client.request.AggregateGroupByPeriodRequest
+import androidx.health.connect.client.request.AggregateRequest
+import androidx.health.connect.client.request.ChangesTokenRequest
+import androidx.health.connect.client.request.ReadRecordsRequest
+import androidx.health.connect.client.time.TimeRangeFilter
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneOffset
+
+fun ReadRecordsRequest<out Record>.toPlatformRequest(
+ timeSource: TimeSource
+): ReadRecordsRequestUsingFilters<out PlatformRecord> {
+ return ReadRecordsRequestUsingFilters.Builder(recordType.toPlatformRecordClass())
+ .setTimeRangeFilter(timeRangeFilter.toPlatformTimeRangeFilter(timeSource))
+ .setAscending(ascendingOrder)
+ .setPageSize(pageSize)
+ .apply {
+ dataOriginFilter.forEach { addDataOrigins(it.toPlatformDataOrigin()) }
+ pageToken?.let { setPageToken(pageToken.toLong()) }
+ }
+ .build()
+}
+
+fun TimeRangeFilter.toPlatformTimeRangeFilter(timeSource: TimeSource): PlatformTimeRangeFilter {
+ // TODO(b/272760519): Remove handling for nullable fields in the first two branches. Needed as
+ // the values used in the underlining implementation cause long overflow
+ return if (startTime != null || endTime != null) {
+ TimeInstantRangeFilter.Builder()
+ .setStartTime(startTime ?: Instant.EPOCH)
+ .setEndTime(endTime ?: timeSource.now)
+ .build()
+ } else if (localStartTime != null || localEndTime != null) {
+ LocalTimeRangeFilter.Builder()
+ .setStartTime(localStartTime ?: LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.MIN))
+ .setEndTime(localEndTime ?: LocalDateTime.ofInstant(timeSource.now, ZoneOffset.MAX))
+ .build()
+ } else {
+ TimeInstantRangeFilter.Builder()
+ .setStartTime(Instant.EPOCH)
+ .setEndTime(timeSource.now)
+ .build()
+ }
+}
+
+fun ChangesTokenRequest.toPlatformRequest(): ChangeLogTokenRequest {
+ return ChangeLogTokenRequest.Builder()
+ .apply {
+ dataOriginFilters.forEach { addDataOriginFilter(it.toPlatformDataOrigin()) }
+ recordTypes.forEach { addRecordType(it.toPlatformRecordClass()) }
+ }
+ .build()
+}
+
+fun AggregateRequest.toPlatformRequest(timeSource: TimeSource): AggregateRecordsRequest<Any> {
+ return AggregateRecordsRequest.Builder<Any>(
+ timeRangeFilter.toPlatformTimeRangeFilter(timeSource)
+ )
+ .apply {
+ dataOriginFilter.forEach { addDataOriginsFilter(it.toPlatformDataOrigin()) }
+ metrics.forEach { addAggregationType(it.toAggregationType()) }
+ }
+ .build()
+}
+
+fun AggregateGroupByDurationRequest.toPlatformRequest(
+ timeSource: TimeSource
+): AggregateRecordsRequest<Any> {
+ return AggregateRecordsRequest.Builder<Any>(
+ timeRangeFilter.toPlatformTimeRangeFilter(timeSource)
+ )
+ .apply {
+ dataOriginFilter.forEach { addDataOriginsFilter(it.toPlatformDataOrigin()) }
+ metrics.forEach { addAggregationType(it.toAggregationType()) }
+ }
+ .build()
+}
+
+fun AggregateGroupByPeriodRequest.toPlatformRequest(
+ timeSource: TimeSource
+): AggregateRecordsRequest<Any> {
+ return AggregateRecordsRequest.Builder<Any>(
+ timeRangeFilter.toPlatformTimeRangeFilter(timeSource)
+ )
+ .apply {
+ dataOriginFilter.forEach { addDataOriginsFilter(it.toPlatformDataOrigin()) }
+ metrics.forEach { addAggregationType(it.toAggregationType()) }
+ }
+ .build()
+}
+
+@Suppress("UNCHECKED_CAST")
+fun AggregateMetric<Any>.toAggregationType(): AggregationType<Any> {
+ return DOUBLE_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
+ ?: DURATION_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
+ ?: ENERGY_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
+ ?: LENGTH_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
+ ?: LONG_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
+ ?: MASS_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
+ ?: POWER_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
+ ?: VOLUME_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
+ ?: throw IllegalArgumentException("Unsupported aggregation type $metricKey")
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/ResponseConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/ResponseConverters.kt
new file mode 100644
index 0000000..31228ad
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/ResponseConverters.kt
@@ -0,0 +1,116 @@
+/*
+ * 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:RestrictTo(RestrictTo.Scope.LIBRARY)
+@file:RequiresApi(api = 34)
+
+package androidx.health.connect.client.impl.platform.records
+
+import android.health.connect.AggregateRecordsGroupedByDurationResponse
+import android.health.connect.AggregateRecordsGroupedByPeriodResponse
+import android.health.connect.AggregateRecordsResponse
+import android.health.connect.datatypes.AggregationType
+import android.health.connect.datatypes.units.Energy as PlatformEnergy
+import android.health.connect.datatypes.units.Volume as PlatformVolume
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.annotation.VisibleForTesting
+import androidx.health.connect.client.aggregate.AggregateMetric
+import androidx.health.connect.client.aggregate.AggregationResult
+import androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration
+import androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod
+import androidx.health.connect.client.units.Energy
+import java.time.ZoneOffset
+
+fun AggregateRecordsResponse<Any>.toSdkResponse(metrics: Set<AggregateMetric<Any>>) =
+ buildAggregationResult(metrics, ::get)
+
+fun AggregateRecordsGroupedByDurationResponse<Any>.toSdkResponse(
+ metrics: Set<AggregateMetric<Any>>
+) =
+ AggregationResultGroupedByDuration(
+ buildAggregationResult(metrics, ::get),
+ startTime,
+ endTime,
+ getZoneOffset(metrics.first().toAggregationType())
+ ?: ZoneOffset.systemDefault().rules.getOffset(startTime)
+ )
+
+fun AggregateRecordsGroupedByPeriodResponse<Any>.toSdkResponse(metrics: Set<AggregateMetric<Any>>) =
+ AggregationResultGroupedByPeriod(buildAggregationResult(metrics, ::get), startTime, endTime)
+
+private fun buildAggregationResult(
+ metrics: Set<AggregateMetric<Any>>,
+ aggregationValueGetter: (AggregationType<Any>) -> Any?
+): AggregationResult {
+ val metricValueMap = buildMap {
+ metrics.forEach { metric ->
+ aggregationValueGetter(metric.toAggregationType())?.also { this[metric] = it }
+ }
+ }
+ return AggregationResult(
+ getLongMetricValues(metricValueMap),
+ getDoubleMetricValues(metricValueMap),
+ setOf()
+ )
+}
+
+@VisibleForTesting
+internal fun getLongMetricValues(
+ metricValueMap: Map<AggregateMetric<Any>, Any>
+): Map<String, Long> {
+ return buildMap {
+ metricValueMap.forEach { (key, value) ->
+ if (
+ key in DURATION_AGGREGATION_METRIC_TYPE_MAP ||
+ key in LONG_AGGREGATION_METRIC_TYPE_MAP
+ ) {
+ this[key.metricKey] = value as Long
+ }
+ }
+ }
+}
+
+@VisibleForTesting
+internal fun getDoubleMetricValues(
+ metricValueMap: Map<AggregateMetric<Any>, Any>
+): Map<String, Double> {
+ return buildMap {
+ metricValueMap.forEach { (key, value) ->
+ when (key) {
+ in DOUBLE_AGGREGATION_METRIC_TYPE_MAP -> {
+ this[key.metricKey] = value as Double
+ }
+ in ENERGY_AGGREGATION_METRIC_TYPE_MAP -> {
+ this[key.metricKey] =
+ Energy.calories((value as PlatformEnergy).inCalories).inKilocalories
+ }
+ in LENGTH_AGGREGATION_METRIC_TYPE_MAP -> {
+ this[key.metricKey] = (value as PlatformLength).inMeters
+ }
+ in MASS_AGGREGATION_METRIC_TYPE_MAP -> {
+ this[key.metricKey] = (value as PlatformMass).inGrams
+ }
+ in POWER_AGGREGATION_METRIC_TYPE_MAP -> {
+ this[key.metricKey] = (value as PlatformPower).inWatts
+ }
+ in VOLUME_AGGREGATION_METRIC_TYPE_MAP -> {
+ this[key.metricKey] = (value as PlatformVolume).inLiters
+ }
+ }
+ }
+ }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/UnitConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/UnitConverters.kt
new file mode 100644
index 0000000..c9a23f9
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/UnitConverters.kt
@@ -0,0 +1,114 @@
+/*
+ * 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:RestrictTo(RestrictTo.Scope.LIBRARY)
+@file:RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+
+package androidx.health.connect.client.impl.platform.records
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.units.BloodGlucose
+import androidx.health.connect.client.units.Energy
+import androidx.health.connect.client.units.Length
+import androidx.health.connect.client.units.Mass
+import androidx.health.connect.client.units.Percentage
+import androidx.health.connect.client.units.Power
+import androidx.health.connect.client.units.Pressure
+import androidx.health.connect.client.units.Temperature
+import androidx.health.connect.client.units.Velocity
+import androidx.health.connect.client.units.Volume
+
+internal fun BloodGlucose.toPlatformBloodGlucose(): PlatformBloodGlucose {
+ return PlatformBloodGlucose.fromMillimolesPerLiter(inMillimolesPerLiter)
+}
+
+internal fun Energy.toPlatformEnergy(): PlatformEnergy {
+ return PlatformEnergy.fromCalories(inCalories)
+}
+
+internal fun Length.toPlatformLength(): PlatformLength {
+ return PlatformLength.fromMeters(inMeters)
+}
+
+internal fun Mass.toPlatformMass(): PlatformMass {
+ return PlatformMass.fromGrams(inGrams)
+}
+
+internal fun Percentage.toPlatformPercentage(): PlatformPercentage {
+ return PlatformPercentage.fromValue(value)
+}
+
+internal fun Power.toPlatformPower(): PlatformPower {
+ return PlatformPower.fromWatts(inWatts)
+}
+
+internal fun Pressure.toPlatformPressure(): PlatformPressure {
+ return PlatformPressure.fromMillimetersOfMercury(inMillimetersOfMercury)
+}
+
+internal fun Temperature.toPlatformTemperature(): PlatformTemperature {
+ return PlatformTemperature.fromCelsius(inCelsius)
+}
+
+internal fun Velocity.toPlatformVelocity(): PlatformVelocity {
+ return PlatformVelocity.fromMetersPerSecond(inMetersPerSecond)
+}
+
+internal fun Volume.toPlatformVolume(): PlatformVolume {
+ return PlatformVolume.fromLiters(inLiters)
+}
+
+internal fun PlatformBloodGlucose.toSdkBloodGlucose(): BloodGlucose {
+ return BloodGlucose.millimolesPerLiter(inMillimolesPerLiter)
+}
+
+internal fun PlatformEnergy.toSdkEnergy(): Energy {
+ return Energy.calories(inCalories)
+}
+
+internal fun PlatformLength.toSdkLength(): Length {
+ return Length.meters(inMeters)
+}
+
+internal fun PlatformMass.toSdkMass(): Mass {
+ return Mass.grams(inGrams)
+}
+
+internal fun PlatformPercentage.toSdkPercentage(): Percentage {
+ return Percentage(value)
+}
+
+internal fun PlatformPower.toSdkPower(): Power {
+ return Power.watts(inWatts)
+}
+
+internal fun PlatformPressure.toSdkPressure(): Pressure {
+ return Pressure.millimetersOfMercury(inMillimetersOfMercury)
+}
+
+internal fun PlatformTemperature.toSdkTemperature(): Temperature {
+ return Temperature.celsius(inCelsius)
+}
+
+internal fun PlatformVelocity.toSdkVelocity(): Velocity {
+ return Velocity.metersPerSecond(inMetersPerSecond)
+}
+
+internal fun PlatformVolume.toSdkVolume(): Volume {
+ return Volume.liters(inLiters)
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/package-info.java
similarity index 62%
copy from appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java
copy to health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/package-info.java
index c01917e..c5b8a8e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/package-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Android Open Source Project
+ * Copyright (C) 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.
@@ -14,15 +14,12 @@
* limitations under the License.
*/
-package androidx.appsearch.observer;
-
-import androidx.annotation.RestrictTo;
-
/**
- * @deprecated use {@link ObserverCallback} instead.
+ * Helps with conversions to the platform record and API objects.
+ *
* @hide
*/
-// TODO(b/209734214): Remove this after dogfooders and devices have migrated away from this class.
-@Deprecated
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public interface AppSearchObserverCallback extends ObserverCallback {}
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.health.connect.client.impl.platform.records;
+
+import androidx.annotation.RestrictTo;
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/InsertRecordsResponseConverter.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/InsertRecordsResponseConverter.kt
new file mode 100644
index 0000000..10903a2
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/InsertRecordsResponseConverter.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:RequiresApi(api = 34)
+
+package androidx.health.connect.client.impl.platform.response
+
+import android.health.connect.InsertRecordsResponse
+import androidx.annotation.RequiresApi
+
+internal fun InsertRecordsResponse.toKtResponse():
+ androidx.health.connect.client.response.InsertRecordsResponse {
+ return androidx.health.connect.client.response.InsertRecordsResponse(
+ recordIdsList = records.map { record -> record.metadata.id }
+ )
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/package-info.java
similarity index 62%
copy from appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java
copy to health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/package-info.java
index c01917e..f8b9cb7 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/package-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Android Open Source Project
+ * Copyright (C) 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.
@@ -14,15 +14,12 @@
* limitations under the License.
*/
-package androidx.appsearch.observer;
-
-import androidx.annotation.RestrictTo;
-
/**
- * @deprecated use {@link ObserverCallback} instead.
+ * Helps with conversions to the platform record and API objects.
+ *
* @hide
*/
-// TODO(b/209734214): Remove this after dogfooders and devices have migrated away from this class.
-@Deprecated
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public interface AppSearchObserverCallback extends ObserverCallback {}
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.health.connect.client.impl.platform.response;
+
+import androidx.annotation.RestrictTo;
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/time/TimeSource.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/time/TimeSource.kt
new file mode 100644
index 0000000..fb3561c
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/time/TimeSource.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:RestrictTo(RestrictTo.Scope.LIBRARY)
+@file:RequiresApi(api = 34)
+
+package androidx.health.connect.client.impl.platform.time
+
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import java.time.Instant
+
+interface TimeSource {
+ val now: Instant
+}
+
+object SystemDefaultTimeSource : TimeSource {
+ override val now: Instant
+ get() = Instant.now()
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
index 5103509..d09bd4e 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
@@ -129,138 +129,128 @@
return WRITE_PERMISSION_PREFIX + RECORD_TYPE_TO_PERMISSION.getOrDefault(recordType, "")
}
+ internal const val PERMISSION_PREFIX = "android.permission.health."
+
// Read permissions for ACTIVITY.
internal const val READ_ACTIVE_CALORIES_BURNED =
- "android.permission.health.READ_ACTIVE_CALORIES_BURNED"
- internal const val READ_DISTANCE = "android.permission.health.READ_DISTANCE"
- internal const val READ_ELEVATION_GAINED = "android.permission.health.READ_ELEVATION_GAINED"
- internal const val READ_EXERCISE = "android.permission.health.READ_EXERCISE"
- internal const val READ_FLOORS_CLIMBED = "android.permission.health.READ_FLOORS_CLIMBED"
- internal const val READ_STEPS = "android.permission.health.READ_STEPS"
+ PERMISSION_PREFIX + "READ_ACTIVE_CALORIES_BURNED"
+ internal const val READ_DISTANCE = PERMISSION_PREFIX + "READ_DISTANCE"
+ internal const val READ_ELEVATION_GAINED = PERMISSION_PREFIX + "READ_ELEVATION_GAINED"
+ internal const val READ_EXERCISE = PERMISSION_PREFIX + "READ_EXERCISE"
+ internal const val READ_FLOORS_CLIMBED = PERMISSION_PREFIX + "READ_FLOORS_CLIMBED"
+ internal const val READ_STEPS = PERMISSION_PREFIX + "READ_STEPS"
internal const val READ_TOTAL_CALORIES_BURNED =
- "android.permission.health.READ_TOTAL_CALORIES_BURNED"
- internal const val READ_VO2_MAX = "android.permission.health.READ_VO2_MAX"
- internal const val READ_WHEELCHAIR_PUSHES =
- "android.permission.health.READ_WHEELCHAIR_PUSHES"
- internal const val READ_POWER = "android.permission.health.READ_POWER"
- internal const val READ_SPEED = "android.permission.health.READ_SPEED"
+ PERMISSION_PREFIX + "READ_TOTAL_CALORIES_BURNED"
+ internal const val READ_VO2_MAX = PERMISSION_PREFIX + "READ_VO2_MAX"
+ internal const val READ_WHEELCHAIR_PUSHES = PERMISSION_PREFIX + "READ_WHEELCHAIR_PUSHES"
+ internal const val READ_POWER = PERMISSION_PREFIX + "READ_POWER"
+ internal const val READ_SPEED = PERMISSION_PREFIX + "READ_SPEED"
// Read permissions for BODY_MEASUREMENTS.
internal const val READ_BASAL_METABOLIC_RATE =
- "android.permission.health.READ_BASAL_METABOLIC_RATE"
- internal const val READ_BODY_FAT = "android.permission.health.READ_BODY_FAT"
- internal const val READ_BODY_WATER_MASS = "android.permission.health.READ_BODY_WATER_MASS"
- internal const val READ_BONE_MASS = "android.permission.health.READ_BONE_MASS"
- internal const val READ_HEIGHT = "android.permission.health.READ_HEIGHT"
+ PERMISSION_PREFIX + "READ_BASAL_METABOLIC_RATE"
+ internal const val READ_BODY_FAT = PERMISSION_PREFIX + "READ_BODY_FAT"
+ internal const val READ_BODY_WATER_MASS = PERMISSION_PREFIX + "READ_BODY_WATER_MASS"
+ internal const val READ_BONE_MASS = PERMISSION_PREFIX + "READ_BONE_MASS"
+ internal const val READ_HEIGHT = PERMISSION_PREFIX + "READ_HEIGHT"
@RestrictTo(RestrictTo.Scope.LIBRARY)
- internal const val READ_HIP_CIRCUMFERENCE =
- "android.permission.health.READ_HIP_CIRCUMFERENCE"
- internal const val READ_LEAN_BODY_MASS = "android.permission.health.READ_LEAN_BODY_MASS"
+ internal const val READ_HIP_CIRCUMFERENCE = PERMISSION_PREFIX + "READ_HIP_CIRCUMFERENCE"
+ internal const val READ_LEAN_BODY_MASS = PERMISSION_PREFIX + "READ_LEAN_BODY_MASS"
@RestrictTo(RestrictTo.Scope.LIBRARY)
- internal const val READ_WAIST_CIRCUMFERENCE =
- "android.permission.health.READ_WAIST_CIRCUMFERENCE"
- internal const val READ_WEIGHT = "android.permission.health.READ_WEIGHT"
+ internal const val READ_WAIST_CIRCUMFERENCE = PERMISSION_PREFIX + "READ_WAIST_CIRCUMFERENCE"
+ internal const val READ_WEIGHT = PERMISSION_PREFIX + "READ_WEIGHT"
// Read permissions for CYCLE_TRACKING.
- internal const val READ_CERVICAL_MUCUS = "android.permission.health.READ_CERVICAL_MUCUS"
+ internal const val READ_CERVICAL_MUCUS = PERMISSION_PREFIX + "READ_CERVICAL_MUCUS"
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal const val READ_INTERMENSTRUAL_BLEEDING =
- "android.permission.health.READ_INTERMENSTRUAL_BLEEDING"
- internal const val READ_MENSTRUATION = "android.permission.health.READ_MENSTRUATION"
- internal const val READ_OVULATION_TEST = "android.permission.health.READ_OVULATION_TEST"
- internal const val READ_SEXUAL_ACTIVITY = "android.permission.health.READ_SEXUAL_ACTIVITY"
+ PERMISSION_PREFIX + "READ_INTERMENSTRUAL_BLEEDING"
+ internal const val READ_MENSTRUATION = PERMISSION_PREFIX + "READ_MENSTRUATION"
+ internal const val READ_OVULATION_TEST = PERMISSION_PREFIX + "READ_OVULATION_TEST"
+ internal const val READ_SEXUAL_ACTIVITY = PERMISSION_PREFIX + "READ_SEXUAL_ACTIVITY"
// Read permissions for NUTRITION.
- internal const val READ_HYDRATION = "android.permission.health.READ_HYDRATION"
- internal const val READ_NUTRITION = "android.permission.health.READ_NUTRITION"
+ internal const val READ_HYDRATION = PERMISSION_PREFIX + "READ_HYDRATION"
+ internal const val READ_NUTRITION = PERMISSION_PREFIX + "READ_NUTRITION"
// Read permissions for SLEEP.
- internal const val READ_SLEEP = "android.permission.health.READ_SLEEP"
+ internal const val READ_SLEEP = PERMISSION_PREFIX + "READ_SLEEP"
// Read permissions for VITALS.
internal const val READ_BASAL_BODY_TEMPERATURE =
- "android.permission.health.READ_BASAL_BODY_TEMPERATURE"
- internal const val READ_BLOOD_GLUCOSE = "android.permission.health.READ_BLOOD_GLUCOSE"
- internal const val READ_BLOOD_PRESSURE = "android.permission.health.READ_BLOOD_PRESSURE"
- internal const val READ_BODY_TEMPERATURE = "android.permission.health.READ_BODY_TEMPERATURE"
- internal const val READ_HEART_RATE = "android.permission.health.READ_HEART_RATE"
+ PERMISSION_PREFIX + "READ_BASAL_BODY_TEMPERATURE"
+ internal const val READ_BLOOD_GLUCOSE = PERMISSION_PREFIX + "READ_BLOOD_GLUCOSE"
+ internal const val READ_BLOOD_PRESSURE = PERMISSION_PREFIX + "READ_BLOOD_PRESSURE"
+ internal const val READ_BODY_TEMPERATURE = PERMISSION_PREFIX + "READ_BODY_TEMPERATURE"
+ internal const val READ_HEART_RATE = PERMISSION_PREFIX + "READ_HEART_RATE"
internal const val READ_HEART_RATE_VARIABILITY =
- "android.permission.health.READ_HEART_RATE_VARIABILITY"
- internal const val READ_OXYGEN_SATURATION =
- "android.permission.health.READ_OXYGEN_SATURATION"
- internal const val READ_RESPIRATORY_RATE = "android.permission.health.READ_RESPIRATORY_RATE"
- internal const val READ_RESTING_HEART_RATE =
- "android.permission.health.READ_RESTING_HEART_RATE"
+ PERMISSION_PREFIX + "READ_HEART_RATE_VARIABILITY"
+ internal const val READ_OXYGEN_SATURATION = PERMISSION_PREFIX + "READ_OXYGEN_SATURATION"
+ internal const val READ_RESPIRATORY_RATE = PERMISSION_PREFIX + "READ_RESPIRATORY_RATE"
+ internal const val READ_RESTING_HEART_RATE = PERMISSION_PREFIX + "READ_RESTING_HEART_RATE"
// Write permissions for ACTIVITY.
internal const val WRITE_ACTIVE_CALORIES_BURNED =
- "android.permission.health.WRITE_ACTIVE_CALORIES_BURNED"
- internal const val WRITE_DISTANCE = "android.permission.health.WRITE_DISTANCE"
- internal const val WRITE_ELEVATION_GAINED =
- "android.permission.health.WRITE_ELEVATION_GAINED"
- internal const val WRITE_EXERCISE = "android.permission.health.WRITE_EXERCISE"
- internal const val WRITE_EXERCISE_ROUTE = "android.permission.health.WRITE_EXERCISE_ROUTE"
- internal const val WRITE_FLOORS_CLIMBED = "android.permission.health.WRITE_FLOORS_CLIMBED"
- internal const val WRITE_STEPS = "android.permission.health.WRITE_STEPS"
+ PERMISSION_PREFIX + "WRITE_ACTIVE_CALORIES_BURNED"
+ internal const val WRITE_DISTANCE = PERMISSION_PREFIX + "WRITE_DISTANCE"
+ internal const val WRITE_ELEVATION_GAINED = PERMISSION_PREFIX + "WRITE_ELEVATION_GAINED"
+ internal const val WRITE_EXERCISE = PERMISSION_PREFIX + "WRITE_EXERCISE"
+ internal const val WRITE_EXERCISE_ROUTE = PERMISSION_PREFIX + "WRITE_EXERCISE_ROUTE"
+ internal const val WRITE_FLOORS_CLIMBED = PERMISSION_PREFIX + "WRITE_FLOORS_CLIMBED"
+ internal const val WRITE_STEPS = PERMISSION_PREFIX + "WRITE_STEPS"
internal const val WRITE_TOTAL_CALORIES_BURNED =
- "android.permission.health.WRITE_TOTAL_CALORIES_BURNED"
- internal const val WRITE_VO2_MAX = "android.permission.health.WRITE_VO2_MAX"
- internal const val WRITE_WHEELCHAIR_PUSHES =
- "android.permission.health.WRITE_WHEELCHAIR_PUSHES"
- internal const val WRITE_POWER = "android.permission.health.WRITE_POWER"
- internal const val WRITE_SPEED = "android.permission.health.WRITE_SPEED"
+ PERMISSION_PREFIX + "WRITE_TOTAL_CALORIES_BURNED"
+ internal const val WRITE_VO2_MAX = PERMISSION_PREFIX + "WRITE_VO2_MAX"
+ internal const val WRITE_WHEELCHAIR_PUSHES = PERMISSION_PREFIX + "WRITE_WHEELCHAIR_PUSHES"
+ internal const val WRITE_POWER = PERMISSION_PREFIX + "WRITE_POWER"
+ internal const val WRITE_SPEED = PERMISSION_PREFIX + "WRITE_SPEED"
// Write permissions for BODY_MEASUREMENTS.
internal const val WRITE_BASAL_METABOLIC_RATE =
- "android.permission.health.WRITE_BASAL_METABOLIC_RATE"
- internal const val WRITE_BODY_FAT = "android.permission.health.WRITE_BODY_FAT"
- internal const val WRITE_BODY_WATER_MASS = "android.permission.health.WRITE_BODY_WATER_MASS"
- internal const val WRITE_BONE_MASS = "android.permission.health.WRITE_BONE_MASS"
- internal const val WRITE_HEIGHT = "android.permission.health.WRITE_HEIGHT"
+ PERMISSION_PREFIX + "WRITE_BASAL_METABOLIC_RATE"
+ internal const val WRITE_BODY_FAT = PERMISSION_PREFIX + "WRITE_BODY_FAT"
+ internal const val WRITE_BODY_WATER_MASS = PERMISSION_PREFIX + "WRITE_BODY_WATER_MASS"
+ internal const val WRITE_BONE_MASS = PERMISSION_PREFIX + "WRITE_BONE_MASS"
+ internal const val WRITE_HEIGHT = PERMISSION_PREFIX + "WRITE_HEIGHT"
@RestrictTo(RestrictTo.Scope.LIBRARY)
- internal const val WRITE_HIP_CIRCUMFERENCE =
- "android.permission.health.WRITE_HIP_CIRCUMFERENCE"
- internal const val WRITE_LEAN_BODY_MASS = "android.permission.health.WRITE_LEAN_BODY_MASS"
+ internal const val WRITE_HIP_CIRCUMFERENCE = PERMISSION_PREFIX + "WRITE_HIP_CIRCUMFERENCE"
+ internal const val WRITE_LEAN_BODY_MASS = PERMISSION_PREFIX + "WRITE_LEAN_BODY_MASS"
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal const val WRITE_WAIST_CIRCUMFERENCE =
- "android.permission.health.WRITE_WAIST_CIRCUMFERENCE"
- internal const val WRITE_WEIGHT = "android.permission.health.WRITE_WEIGHT"
+ PERMISSION_PREFIX + "WRITE_WAIST_CIRCUMFERENCE"
+ internal const val WRITE_WEIGHT = PERMISSION_PREFIX + "WRITE_WEIGHT"
// Write permissions for CYCLE_TRACKING.
- internal const val WRITE_CERVICAL_MUCUS = "android.permission.health.WRITE_CERVICAL_MUCUS"
+ internal const val WRITE_CERVICAL_MUCUS = PERMISSION_PREFIX + "WRITE_CERVICAL_MUCUS"
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal const val WRITE_INTERMENSTRUAL_BLEEDING =
- "android.permission.health.WRITE_INTERMENSTRUAL_BLEEDING"
- internal const val WRITE_MENSTRUATION = "android.permission.health.WRITE_MENSTRUATION"
- internal const val WRITE_OVULATION_TEST = "android.permission.health.WRITE_OVULATION_TEST"
- internal const val WRITE_SEXUAL_ACTIVITY = "android.permission.health.WRITE_SEXUAL_ACTIVITY"
+ PERMISSION_PREFIX + "WRITE_INTERMENSTRUAL_BLEEDING"
+ internal const val WRITE_MENSTRUATION = PERMISSION_PREFIX + "WRITE_MENSTRUATION"
+ internal const val WRITE_OVULATION_TEST = PERMISSION_PREFIX + "WRITE_OVULATION_TEST"
+ internal const val WRITE_SEXUAL_ACTIVITY = PERMISSION_PREFIX + "WRITE_SEXUAL_ACTIVITY"
// Write permissions for NUTRITION.
- internal const val WRITE_HYDRATION = "android.permission.health.WRITE_HYDRATION"
- internal const val WRITE_NUTRITION = "android.permission.health.WRITE_NUTRITION"
+ internal const val WRITE_HYDRATION = PERMISSION_PREFIX + "WRITE_HYDRATION"
+ internal const val WRITE_NUTRITION = PERMISSION_PREFIX + "WRITE_NUTRITION"
// Write permissions for SLEEP.
- internal const val WRITE_SLEEP = "android.permission.health.WRITE_SLEEP"
+ internal const val WRITE_SLEEP = PERMISSION_PREFIX + "WRITE_SLEEP"
// Write permissions for VITALS.
internal const val WRITE_BASAL_BODY_TEMPERATURE =
- "android.permission.health.WRITE_BASAL_BODY_TEMPERATURE"
- internal const val WRITE_BLOOD_GLUCOSE = "android.permission.health.WRITE_BLOOD_GLUCOSE"
- internal const val WRITE_BLOOD_PRESSURE = "android.permission.health.WRITE_BLOOD_PRESSURE"
- internal const val WRITE_BODY_TEMPERATURE =
- "android.permission.health.WRITE_BODY_TEMPERATURE"
- internal const val WRITE_HEART_RATE = "android.permission.health.WRITE_HEART_RATE"
+ PERMISSION_PREFIX + "WRITE_BASAL_BODY_TEMPERATURE"
+ internal const val WRITE_BLOOD_GLUCOSE = PERMISSION_PREFIX + "WRITE_BLOOD_GLUCOSE"
+ internal const val WRITE_BLOOD_PRESSURE = PERMISSION_PREFIX + "WRITE_BLOOD_PRESSURE"
+ internal const val WRITE_BODY_TEMPERATURE = PERMISSION_PREFIX + "WRITE_BODY_TEMPERATURE"
+ internal const val WRITE_HEART_RATE = PERMISSION_PREFIX + "WRITE_HEART_RATE"
internal const val WRITE_HEART_RATE_VARIABILITY =
- "android.permission.health.WRITE_HEART_RATE_VARIABILITY"
- internal const val WRITE_OXYGEN_SATURATION =
- "android.permission.health.WRITE_OXYGEN_SATURATION"
- internal const val WRITE_RESPIRATORY_RATE =
- "android.permission.health.WRITE_RESPIRATORY_RATE"
- internal const val WRITE_RESTING_HEART_RATE =
- "android.permission.health.WRITE_RESTING_HEART_RATE"
+ PERMISSION_PREFIX + "WRITE_HEART_RATE_VARIABILITY"
+ internal const val WRITE_OXYGEN_SATURATION = PERMISSION_PREFIX + "WRITE_OXYGEN_SATURATION"
+ internal const val WRITE_RESPIRATORY_RATE = PERMISSION_PREFIX + "WRITE_RESPIRATORY_RATE"
+ internal const val WRITE_RESTING_HEART_RATE = PERMISSION_PREFIX + "WRITE_RESTING_HEART_RATE"
- internal const val READ_PERMISSION_PREFIX = "android.permission.health.READ_"
- internal const val WRITE_PERMISSION_PREFIX = "android.permission.health.WRITE_"
+ internal const val READ_PERMISSION_PREFIX = PERMISSION_PREFIX + "READ_"
+ internal const val WRITE_PERMISSION_PREFIX = PERMISSION_PREFIX + "WRITE_"
internal val RECORD_TYPE_TO_PERMISSION =
mapOf<KClass<out Record>, String>(
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCake.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCake.kt
new file mode 100644
index 0000000..e6994c5
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCake.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 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.health.connect.client.permission.platform
+
+import android.content.Context
+import android.content.Intent
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_PREFIX
+
+/**
+ * An [ActivityResultContract] to request Health Connect system permissions.
+ *
+ * @see androidx.activity.ComponentActivity.registerForActivityResult
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+internal class HealthDataRequestPermissionsUpsideDownCake :
+ ActivityResultContract<Set<String>, Set<String>>() {
+
+ private val requestPermissions = RequestMultiplePermissions()
+
+ override fun createIntent(context: Context, input: Set<String>): Intent {
+ require(input.all { it.startsWith(PERMISSION_PREFIX) }) {
+ "Unsupported health connect permission"
+ }
+ return requestPermissions.createIntent(context, input.toTypedArray())
+ }
+
+ override fun parseResult(resultCode: Int, intent: Intent?): Set<String> =
+ requestPermissions.parseResult(resultCode, intent).filterValues { it }.keys
+
+ override fun getSynchronousResult(
+ context: Context,
+ input: Set<String>,
+ ): SynchronousResult<Set<String>>? =
+ requestPermissions.getSynchronousResult(context, input.toTypedArray())?.let { result ->
+ SynchronousResult(result.value.filterValues { it }.keys)
+ }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyTemperatureMeasurementLocation.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyTemperatureMeasurementLocation.kt
index 6b77845..1b4c836 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyTemperatureMeasurementLocation.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyTemperatureMeasurementLocation.kt
@@ -67,8 +67,10 @@
/**
* Where on the user's body a temperature measurement was taken from.
+ *
* @suppress
*/
+@Target(AnnotationTarget.PROPERTY, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.SOURCE)
@IntDef(
value =
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
index d839bb1..0fb3935 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
@@ -21,6 +21,7 @@
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.os.Build
+import androidx.health.connect.client.impl.HealthConnectClientImpl
import androidx.health.platform.client.HealthDataService
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -97,15 +98,14 @@
context,
HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME,
versionCode = HealthConnectClient.DEFAULT_PROVIDER_MIN_VERSION_CODE - 1,
- enabled = true)
+ enabled = true
+ )
installService(context, HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME)
assertThat(HealthConnectClient.isProviderAvailable(context)).isFalse()
assertThat(HealthConnectClient.getSdkStatus(context, PROVIDER_PACKAGE_NAME))
.isEqualTo(HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED)
- assertThrows(IllegalStateException::class.java) {
- HealthConnectClient.getOrCreate(context)
- }
+ assertThrows(IllegalStateException::class.java) { HealthConnectClient.getOrCreate(context) }
}
@Test
@@ -116,14 +116,40 @@
context,
HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME,
versionCode = HealthConnectClient.DEFAULT_PROVIDER_MIN_VERSION_CODE,
- enabled = true)
+ enabled = true
+ )
installService(context, HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME)
assertThat(HealthConnectClient.isProviderAvailable(context)).isTrue()
assertThat(HealthConnectClient.getSdkStatus(
context, HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME))
.isEqualTo(HealthConnectClient.SDK_AVAILABLE)
- HealthConnectClient.getOrCreate(context)
+ assertThat(HealthConnectClient.getOrCreate(context))
+ .isInstanceOf(HealthConnectClientImpl::class.java)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.P])
+ @Suppress("Deprecation")
+ fun backingImplementationLegacy_enabledSupportedVersion_isAvailable() {
+ installPackage(
+ context,
+ HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME,
+ versionCode = HealthConnectClient.DEFAULT_PROVIDER_MIN_VERSION_CODE,
+ enabled = true
+ )
+ installService(context, HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME)
+
+ assertThat(HealthConnectClient.isProviderAvailableLegacy(context)).isTrue()
+ assertThat(
+ HealthConnectClient.getSdkStatusLegacy(
+ context,
+ HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME
+ )
+ )
+ .isEqualTo(HealthConnectClient.SDK_AVAILABLE)
+ assertThat(HealthConnectClient.getOrCreateLegacy(context))
+ .isInstanceOf(HealthConnectClientImpl::class.java)
}
@Test
@@ -140,6 +166,20 @@
}
}
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.O_MR1])
+ @Suppress("Deprecation")
+ fun sdkVersionTooOld_legacyClient_unavailable() {
+ assertThat(HealthConnectClient.isApiSupported()).isFalse()
+ assertThat(HealthConnectClient.isProviderAvailableLegacy(context, PROVIDER_PACKAGE_NAME))
+ .isFalse()
+ assertThat(HealthConnectClient.getSdkStatusLegacy(context, PROVIDER_PACKAGE_NAME))
+ .isEqualTo(HealthConnectClient.SDK_UNAVAILABLE)
+ assertThrows(UnsupportedOperationException::class.java) {
+ HealthConnectClient.getOrCreateLegacy(context, PROVIDER_PACKAGE_NAME)
+ }
+ }
+
private fun installPackage(
context: Context,
packageName: String,
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/PermissionControllerTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/PermissionControllerTest.kt
index f0d031bc..00482f1 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/PermissionControllerTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/PermissionControllerTest.kt
@@ -17,13 +17,16 @@
package androidx.health.connect.client
import android.content.Context
+import android.os.Build.VERSION_CODES
+import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
import androidx.health.connect.client.permission.HealthPermission
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
private const val PROVIDER_PACKAGE_NAME = "com.example.fake.provider"
@@ -38,7 +41,7 @@
}
@Test
- fun createIntentTest_permissionStrings() {
+ fun createIntent_permissionStrings() {
val requestPermissionContract =
PermissionController.createRequestPermissionResultContract(PROVIDER_PACKAGE_NAME)
val intent =
@@ -47,7 +50,40 @@
setOf(HealthPermission.READ_ACTIVE_CALORIES_BURNED)
)
- Truth.assertThat(intent.action).isEqualTo("androidx.health.ACTION_REQUEST_PERMISSIONS")
- Truth.assertThat(intent.`package`).isEqualTo(PROVIDER_PACKAGE_NAME)
+ assertThat(intent.action).isEqualTo("androidx.health.ACTION_REQUEST_PERMISSIONS")
+ assertThat(intent.`package`).isEqualTo(PROVIDER_PACKAGE_NAME)
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun createIntent_UpsideDownCake() {
+ val requestPermissionContract =
+ PermissionController.createRequestPermissionResultContract(PROVIDER_PACKAGE_NAME)
+ val intent =
+ requestPermissionContract.createIntent(
+ context,
+ setOf(HealthPermission.WRITE_STEPS, HealthPermission.READ_DISTANCE)
+ )
+
+ assertThat(intent.action).isEqualTo(RequestMultiplePermissions.ACTION_REQUEST_PERMISSIONS)
+ assertThat(intent.getStringArrayExtra(RequestMultiplePermissions.EXTRA_PERMISSIONS))
+ .asList()
+ .containsExactly(HealthPermission.WRITE_STEPS, HealthPermission.READ_DISTANCE)
+ assertThat(intent.`package`).isNull()
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun createIntentLegacy_UpsideDownCake() {
+ val requestPermissionContract =
+ PermissionController.createRequestPermissionResultContractLegacy(PROVIDER_PACKAGE_NAME)
+ val intent =
+ requestPermissionContract.createIntent(
+ context,
+ setOf(HealthPermission.WRITE_STEPS, HealthPermission.READ_DISTANCE)
+ )
+
+ assertThat(intent.action).isEqualTo("androidx.health.ACTION_REQUEST_PERMISSIONS")
+ assertThat(intent.`package`).isEqualTo(PROVIDER_PACKAGE_NAME)
}
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCakeTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCakeTest.kt
new file mode 100644
index 0000000..2bbd4f4
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCakeTest.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.health.connect.client.permission.platform
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
+import androidx.health.connect.client.permission.HealthPermission
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class HealthDataRequestPermissionsUpsideDownCakeTest {
+
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ context = ApplicationProvider.getApplicationContext()
+ }
+
+ @Test
+ fun createIntent() {
+ val requestPermissionContract = HealthDataRequestPermissionsUpsideDownCake()
+ val intent =
+ requestPermissionContract.createIntent(
+ context, setOf(HealthPermission.READ_STEPS, HealthPermission.WRITE_DISTANCE))
+
+ assertThat(intent.action).isEqualTo(RequestMultiplePermissions.ACTION_REQUEST_PERMISSIONS)
+ assertThat(intent.getStringArrayExtra(RequestMultiplePermissions.EXTRA_PERMISSIONS))
+ .asList()
+ .containsExactly(HealthPermission.READ_STEPS, HealthPermission.WRITE_DISTANCE)
+ }
+
+ @Test
+ fun createIntent_nonHealthPermission_throwsIAE() {
+ val requestPermissionContract = HealthDataRequestPermissionsUpsideDownCake()
+ assertFailsWith<IllegalArgumentException> {
+ requestPermissionContract.createIntent(
+ context, setOf(HealthPermission.READ_STEPS, "NON_HEALTH_PERMISSION"))
+ }
+ }
+
+ @Test
+ fun parseIntent() {
+ val requestPermissionContract = HealthDataRequestPermissionsUpsideDownCake()
+
+ val intent = Intent()
+ intent.putExtra(
+ RequestMultiplePermissions.EXTRA_PERMISSIONS,
+ arrayOf(
+ HealthPermission.READ_STEPS,
+ HealthPermission.WRITE_STEPS,
+ HealthPermission.WRITE_DISTANCE,
+ HealthPermission.READ_HEART_RATE))
+ intent.putExtra(
+ RequestMultiplePermissions.EXTRA_PERMISSION_GRANT_RESULTS,
+ intArrayOf(
+ PackageManager.PERMISSION_GRANTED,
+ PackageManager.PERMISSION_DENIED,
+ PackageManager.PERMISSION_GRANTED,
+ PackageManager.PERMISSION_DENIED))
+
+ val result = requestPermissionContract.parseResult(Activity.RESULT_OK, intent)
+
+ assertThat(result)
+ .containsExactly(HealthPermission.READ_STEPS, HealthPermission.WRITE_DISTANCE)
+ }
+}
diff --git a/health/connect/connect-client/src/test/resources/robolectric.properties b/health/connect/connect-client/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..69fde47
--- /dev/null
+++ b/health/connect/connect-client/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/health/health-services-client/build.gradle b/health/health-services-client/build.gradle
index 65a44a0..a0df417 100644
--- a/health/health-services-client/build.gradle
+++ b/health/health-services-client/build.gradle
@@ -79,7 +79,7 @@
}
androidx {
- name = "AndroidX Health Services Client Library"
+ name = "Health Services Client"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.HEALTH_SERVICES_CLIENT
inceptionYear = "2021"
diff --git a/health/health-services-client/src/test/resources/robolectric.properties b/health/health-services-client/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..69fde47
--- /dev/null
+++ b/health/health-services-client/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/heifwriter/heifwriter/api/api_lint.ignore b/heifwriter/heifwriter/api/api_lint.ignore
index a93261a..d3c7e43 100644
--- a/heifwriter/heifwriter/api/api_lint.ignore
+++ b/heifwriter/heifwriter/api/api_lint.ignore
@@ -1,42 +1,24 @@
// Baseline format: 1.0
+GenericException: androidx.heifwriter.AvifWriter#stop(long):
+ Methods must not throw generic exceptions (`java.lang.Exception`)
GenericException: androidx.heifwriter.HeifWriter#stop(long):
Methods must not throw generic exceptions (`java.lang.Exception`)
-MissingGetterMatchingBuilder: androidx.heifwriter.HeifWriter.Builder#setGridEnabled(boolean):
- androidx.heifwriter.HeifWriter does not declare a `isGridEnabled()` method matching method androidx.heifwriter.HeifWriter.Builder.setGridEnabled(boolean)
-MissingGetterMatchingBuilder: androidx.heifwriter.HeifWriter.Builder#setHandler(android.os.Handler):
- androidx.heifwriter.HeifWriter does not declare a `getHandler()` method matching method androidx.heifwriter.HeifWriter.Builder.setHandler(android.os.Handler)
-MissingGetterMatchingBuilder: androidx.heifwriter.HeifWriter.Builder#setMaxImages(int):
- androidx.heifwriter.HeifWriter does not declare a `getMaxImages()` method matching method androidx.heifwriter.HeifWriter.Builder.setMaxImages(int)
-MissingGetterMatchingBuilder: androidx.heifwriter.HeifWriter.Builder#setPrimaryIndex(int):
- androidx.heifwriter.HeifWriter does not declare a `getPrimaryIndex()` method matching method androidx.heifwriter.HeifWriter.Builder.setPrimaryIndex(int)
-MissingGetterMatchingBuilder: androidx.heifwriter.HeifWriter.Builder#setQuality(int):
- androidx.heifwriter.HeifWriter does not declare a `getQuality()` method matching method androidx.heifwriter.HeifWriter.Builder.setQuality(int)
-MissingGetterMatchingBuilder: androidx.heifwriter.HeifWriter.Builder#setRotation(int):
- androidx.heifwriter.HeifWriter does not declare a `getRotation()` method matching method androidx.heifwriter.HeifWriter.Builder.setRotation(int)
-
-
-MissingNullability: androidx.heifwriter.HeifWriter.Builder#build():
- Missing nullability on method `build` return
-MissingNullability: androidx.heifwriter.HeifWriter.Builder#setGridEnabled(boolean):
- Missing nullability on method `setGridEnabled` return
-MissingNullability: androidx.heifwriter.HeifWriter.Builder#setHandler(android.os.Handler):
- Missing nullability on method `setHandler` return
-MissingNullability: androidx.heifwriter.HeifWriter.Builder#setMaxImages(int):
- Missing nullability on method `setMaxImages` return
-MissingNullability: androidx.heifwriter.HeifWriter.Builder#setPrimaryIndex(int):
- Missing nullability on method `setPrimaryIndex` return
-MissingNullability: androidx.heifwriter.HeifWriter.Builder#setQuality(int):
- Missing nullability on method `setQuality` return
-MissingNullability: androidx.heifwriter.HeifWriter.Builder#setRotation(int):
- Missing nullability on method `setRotation` return
-
-
+UseParcelFileDescriptor: androidx.heifwriter.AvifWriter.Builder#Builder(java.io.FileDescriptor, int, int, int) parameter #0:
+ Must use ParcelFileDescriptor instead of FileDescriptor in parameter fd in androidx.heifwriter.AvifWriter.Builder(java.io.FileDescriptor fd, int width, int height, int inputMode)
UseParcelFileDescriptor: androidx.heifwriter.HeifWriter.Builder#Builder(java.io.FileDescriptor, int, int, int) parameter #0:
Must use ParcelFileDescriptor instead of FileDescriptor in parameter fd in androidx.heifwriter.HeifWriter.Builder(java.io.FileDescriptor fd, int width, int height, int inputMode)
+VisiblySynchronized: androidx.heifwriter.AvifWriter#addBitmap(android.graphics.Bitmap):
+ Internal locks must not be exposed (synchronizing on this or class is still externally observable): method androidx.heifwriter.AvifWriter.addBitmap(android.graphics.Bitmap)
+VisiblySynchronized: androidx.heifwriter.AvifWriter#addYuvBuffer(int, byte[]):
+ Internal locks must not be exposed (synchronizing on this or class is still externally observable): method androidx.heifwriter.AvifWriter.addYuvBuffer(int,byte[])
+VisiblySynchronized: androidx.heifwriter.AvifWriter#setInputEndOfStreamTimestamp(long):
+ Internal locks must not be exposed (synchronizing on this or class is still externally observable): method androidx.heifwriter.AvifWriter.setInputEndOfStreamTimestamp(long)
+VisiblySynchronized: androidx.heifwriter.AvifWriter#stop(long):
+ Internal locks must not be exposed (synchronizing on this or class is still externally observable): method androidx.heifwriter.AvifWriter.stop(long)
VisiblySynchronized: androidx.heifwriter.HeifWriter#addBitmap(android.graphics.Bitmap):
Internal locks must not be exposed (synchronizing on this or class is still externally observable): method androidx.heifwriter.HeifWriter.addBitmap(android.graphics.Bitmap)
VisiblySynchronized: androidx.heifwriter.HeifWriter#addYuvBuffer(int, byte[]):
diff --git a/heifwriter/heifwriter/api/current.txt b/heifwriter/heifwriter/api/current.txt
index 8a45d85..90c95a4 100644
--- a/heifwriter/heifwriter/api/current.txt
+++ b/heifwriter/heifwriter/api/current.txt
@@ -1,30 +1,71 @@
// Signature format: 4.0
package androidx.heifwriter {
+ public final class AvifWriter implements java.lang.AutoCloseable {
+ method public void addBitmap(android.graphics.Bitmap);
+ method public void addExifData(int, byte[], int, int);
+ method public void addYuvBuffer(int, byte[]);
+ method public void close();
+ method public android.os.Handler? getHandler();
+ method public android.view.Surface getInputSurface();
+ method public int getMaxImages();
+ method public int getPrimaryIndex();
+ method public int getQuality();
+ method public int getRotation();
+ method public boolean isGridEnabled();
+ method public boolean isHighBitDepthEnabled();
+ method public void setInputEndOfStreamTimestamp(@IntRange(from=0) long);
+ method public void start();
+ method public void stop(@IntRange(from=0) long) throws java.lang.Exception;
+ field public static final int INPUT_MODE_BITMAP = 2; // 0x2
+ field public static final int INPUT_MODE_BUFFER = 0; // 0x0
+ field public static final int INPUT_MODE_SURFACE = 1; // 0x1
+ }
+
+ public static final class AvifWriter.Builder {
+ ctor public AvifWriter.Builder(String, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ ctor public AvifWriter.Builder(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ method public androidx.heifwriter.AvifWriter build() throws java.io.IOException;
+ method public androidx.heifwriter.AvifWriter.Builder setGridEnabled(boolean);
+ method public androidx.heifwriter.AvifWriter.Builder setHandler(android.os.Handler?);
+ method public androidx.heifwriter.AvifWriter.Builder setHighBitDepthEnabled(boolean);
+ method public androidx.heifwriter.AvifWriter.Builder setMaxImages(@IntRange(from=1) int);
+ method public androidx.heifwriter.AvifWriter.Builder setPrimaryIndex(@IntRange(from=0) int);
+ method public androidx.heifwriter.AvifWriter.Builder setQuality(@IntRange(from=0, to=100) int);
+ method public androidx.heifwriter.AvifWriter.Builder setRotation(@IntRange(from=0) int);
+ }
+
public final class HeifWriter implements java.lang.AutoCloseable {
method public void addBitmap(android.graphics.Bitmap);
method public void addExifData(int, byte[], int, int);
method public void addYuvBuffer(int, byte[]);
method public void close();
+ method public android.os.Handler? getHandler();
method public android.view.Surface getInputSurface();
- method public void setInputEndOfStreamTimestamp(long);
+ method public int getMaxImages();
+ method public int getPrimaryIndex();
+ method public int getQuality();
+ method public int getRotation();
+ method public boolean isGridEnabled();
+ method public boolean isHighBitDepthEnabled();
+ method public void setInputEndOfStreamTimestamp(@IntRange(from=0) long);
method public void start();
- method public void stop(long) throws java.lang.Exception;
+ method public void stop(@IntRange(from=0) long) throws java.lang.Exception;
field public static final int INPUT_MODE_BITMAP = 2; // 0x2
field public static final int INPUT_MODE_BUFFER = 0; // 0x0
field public static final int INPUT_MODE_SURFACE = 1; // 0x1
}
public static final class HeifWriter.Builder {
- ctor public HeifWriter.Builder(String, int, int, int);
- ctor public HeifWriter.Builder(java.io.FileDescriptor, int, int, int);
- method public androidx.heifwriter.HeifWriter! build() throws java.io.IOException;
- method public androidx.heifwriter.HeifWriter.Builder! setGridEnabled(boolean);
- method public androidx.heifwriter.HeifWriter.Builder! setHandler(android.os.Handler?);
- method public androidx.heifwriter.HeifWriter.Builder! setMaxImages(int);
- method public androidx.heifwriter.HeifWriter.Builder! setPrimaryIndex(int);
- method public androidx.heifwriter.HeifWriter.Builder! setQuality(int);
- method public androidx.heifwriter.HeifWriter.Builder! setRotation(int);
+ ctor public HeifWriter.Builder(String, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ ctor public HeifWriter.Builder(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ method public androidx.heifwriter.HeifWriter build() throws java.io.IOException;
+ method public androidx.heifwriter.HeifWriter.Builder setGridEnabled(boolean);
+ method public androidx.heifwriter.HeifWriter.Builder setHandler(android.os.Handler?);
+ method public androidx.heifwriter.HeifWriter.Builder setMaxImages(@IntRange(from=1) int);
+ method public androidx.heifwriter.HeifWriter.Builder setPrimaryIndex(@IntRange(from=0) int);
+ method public androidx.heifwriter.HeifWriter.Builder setQuality(@IntRange(from=0, to=100) int);
+ method public androidx.heifwriter.HeifWriter.Builder setRotation(@IntRange(from=0) int);
}
}
diff --git a/heifwriter/heifwriter/api/public_plus_experimental_current.txt b/heifwriter/heifwriter/api/public_plus_experimental_current.txt
index 8a45d85..90c95a4 100644
--- a/heifwriter/heifwriter/api/public_plus_experimental_current.txt
+++ b/heifwriter/heifwriter/api/public_plus_experimental_current.txt
@@ -1,30 +1,71 @@
// Signature format: 4.0
package androidx.heifwriter {
+ public final class AvifWriter implements java.lang.AutoCloseable {
+ method public void addBitmap(android.graphics.Bitmap);
+ method public void addExifData(int, byte[], int, int);
+ method public void addYuvBuffer(int, byte[]);
+ method public void close();
+ method public android.os.Handler? getHandler();
+ method public android.view.Surface getInputSurface();
+ method public int getMaxImages();
+ method public int getPrimaryIndex();
+ method public int getQuality();
+ method public int getRotation();
+ method public boolean isGridEnabled();
+ method public boolean isHighBitDepthEnabled();
+ method public void setInputEndOfStreamTimestamp(@IntRange(from=0) long);
+ method public void start();
+ method public void stop(@IntRange(from=0) long) throws java.lang.Exception;
+ field public static final int INPUT_MODE_BITMAP = 2; // 0x2
+ field public static final int INPUT_MODE_BUFFER = 0; // 0x0
+ field public static final int INPUT_MODE_SURFACE = 1; // 0x1
+ }
+
+ public static final class AvifWriter.Builder {
+ ctor public AvifWriter.Builder(String, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ ctor public AvifWriter.Builder(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ method public androidx.heifwriter.AvifWriter build() throws java.io.IOException;
+ method public androidx.heifwriter.AvifWriter.Builder setGridEnabled(boolean);
+ method public androidx.heifwriter.AvifWriter.Builder setHandler(android.os.Handler?);
+ method public androidx.heifwriter.AvifWriter.Builder setHighBitDepthEnabled(boolean);
+ method public androidx.heifwriter.AvifWriter.Builder setMaxImages(@IntRange(from=1) int);
+ method public androidx.heifwriter.AvifWriter.Builder setPrimaryIndex(@IntRange(from=0) int);
+ method public androidx.heifwriter.AvifWriter.Builder setQuality(@IntRange(from=0, to=100) int);
+ method public androidx.heifwriter.AvifWriter.Builder setRotation(@IntRange(from=0) int);
+ }
+
public final class HeifWriter implements java.lang.AutoCloseable {
method public void addBitmap(android.graphics.Bitmap);
method public void addExifData(int, byte[], int, int);
method public void addYuvBuffer(int, byte[]);
method public void close();
+ method public android.os.Handler? getHandler();
method public android.view.Surface getInputSurface();
- method public void setInputEndOfStreamTimestamp(long);
+ method public int getMaxImages();
+ method public int getPrimaryIndex();
+ method public int getQuality();
+ method public int getRotation();
+ method public boolean isGridEnabled();
+ method public boolean isHighBitDepthEnabled();
+ method public void setInputEndOfStreamTimestamp(@IntRange(from=0) long);
method public void start();
- method public void stop(long) throws java.lang.Exception;
+ method public void stop(@IntRange(from=0) long) throws java.lang.Exception;
field public static final int INPUT_MODE_BITMAP = 2; // 0x2
field public static final int INPUT_MODE_BUFFER = 0; // 0x0
field public static final int INPUT_MODE_SURFACE = 1; // 0x1
}
public static final class HeifWriter.Builder {
- ctor public HeifWriter.Builder(String, int, int, int);
- ctor public HeifWriter.Builder(java.io.FileDescriptor, int, int, int);
- method public androidx.heifwriter.HeifWriter! build() throws java.io.IOException;
- method public androidx.heifwriter.HeifWriter.Builder! setGridEnabled(boolean);
- method public androidx.heifwriter.HeifWriter.Builder! setHandler(android.os.Handler?);
- method public androidx.heifwriter.HeifWriter.Builder! setMaxImages(int);
- method public androidx.heifwriter.HeifWriter.Builder! setPrimaryIndex(int);
- method public androidx.heifwriter.HeifWriter.Builder! setQuality(int);
- method public androidx.heifwriter.HeifWriter.Builder! setRotation(int);
+ ctor public HeifWriter.Builder(String, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ ctor public HeifWriter.Builder(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ method public androidx.heifwriter.HeifWriter build() throws java.io.IOException;
+ method public androidx.heifwriter.HeifWriter.Builder setGridEnabled(boolean);
+ method public androidx.heifwriter.HeifWriter.Builder setHandler(android.os.Handler?);
+ method public androidx.heifwriter.HeifWriter.Builder setMaxImages(@IntRange(from=1) int);
+ method public androidx.heifwriter.HeifWriter.Builder setPrimaryIndex(@IntRange(from=0) int);
+ method public androidx.heifwriter.HeifWriter.Builder setQuality(@IntRange(from=0, to=100) int);
+ method public androidx.heifwriter.HeifWriter.Builder setRotation(@IntRange(from=0) int);
}
}
diff --git a/heifwriter/heifwriter/api/restricted_current.txt b/heifwriter/heifwriter/api/restricted_current.txt
index 8a45d85..90c95a4 100644
--- a/heifwriter/heifwriter/api/restricted_current.txt
+++ b/heifwriter/heifwriter/api/restricted_current.txt
@@ -1,30 +1,71 @@
// Signature format: 4.0
package androidx.heifwriter {
+ public final class AvifWriter implements java.lang.AutoCloseable {
+ method public void addBitmap(android.graphics.Bitmap);
+ method public void addExifData(int, byte[], int, int);
+ method public void addYuvBuffer(int, byte[]);
+ method public void close();
+ method public android.os.Handler? getHandler();
+ method public android.view.Surface getInputSurface();
+ method public int getMaxImages();
+ method public int getPrimaryIndex();
+ method public int getQuality();
+ method public int getRotation();
+ method public boolean isGridEnabled();
+ method public boolean isHighBitDepthEnabled();
+ method public void setInputEndOfStreamTimestamp(@IntRange(from=0) long);
+ method public void start();
+ method public void stop(@IntRange(from=0) long) throws java.lang.Exception;
+ field public static final int INPUT_MODE_BITMAP = 2; // 0x2
+ field public static final int INPUT_MODE_BUFFER = 0; // 0x0
+ field public static final int INPUT_MODE_SURFACE = 1; // 0x1
+ }
+
+ public static final class AvifWriter.Builder {
+ ctor public AvifWriter.Builder(String, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ ctor public AvifWriter.Builder(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ method public androidx.heifwriter.AvifWriter build() throws java.io.IOException;
+ method public androidx.heifwriter.AvifWriter.Builder setGridEnabled(boolean);
+ method public androidx.heifwriter.AvifWriter.Builder setHandler(android.os.Handler?);
+ method public androidx.heifwriter.AvifWriter.Builder setHighBitDepthEnabled(boolean);
+ method public androidx.heifwriter.AvifWriter.Builder setMaxImages(@IntRange(from=1) int);
+ method public androidx.heifwriter.AvifWriter.Builder setPrimaryIndex(@IntRange(from=0) int);
+ method public androidx.heifwriter.AvifWriter.Builder setQuality(@IntRange(from=0, to=100) int);
+ method public androidx.heifwriter.AvifWriter.Builder setRotation(@IntRange(from=0) int);
+ }
+
public final class HeifWriter implements java.lang.AutoCloseable {
method public void addBitmap(android.graphics.Bitmap);
method public void addExifData(int, byte[], int, int);
method public void addYuvBuffer(int, byte[]);
method public void close();
+ method public android.os.Handler? getHandler();
method public android.view.Surface getInputSurface();
- method public void setInputEndOfStreamTimestamp(long);
+ method public int getMaxImages();
+ method public int getPrimaryIndex();
+ method public int getQuality();
+ method public int getRotation();
+ method public boolean isGridEnabled();
+ method public boolean isHighBitDepthEnabled();
+ method public void setInputEndOfStreamTimestamp(@IntRange(from=0) long);
method public void start();
- method public void stop(long) throws java.lang.Exception;
+ method public void stop(@IntRange(from=0) long) throws java.lang.Exception;
field public static final int INPUT_MODE_BITMAP = 2; // 0x2
field public static final int INPUT_MODE_BUFFER = 0; // 0x0
field public static final int INPUT_MODE_SURFACE = 1; // 0x1
}
public static final class HeifWriter.Builder {
- ctor public HeifWriter.Builder(String, int, int, int);
- ctor public HeifWriter.Builder(java.io.FileDescriptor, int, int, int);
- method public androidx.heifwriter.HeifWriter! build() throws java.io.IOException;
- method public androidx.heifwriter.HeifWriter.Builder! setGridEnabled(boolean);
- method public androidx.heifwriter.HeifWriter.Builder! setHandler(android.os.Handler?);
- method public androidx.heifwriter.HeifWriter.Builder! setMaxImages(int);
- method public androidx.heifwriter.HeifWriter.Builder! setPrimaryIndex(int);
- method public androidx.heifwriter.HeifWriter.Builder! setQuality(int);
- method public androidx.heifwriter.HeifWriter.Builder! setRotation(int);
+ ctor public HeifWriter.Builder(String, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ ctor public HeifWriter.Builder(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ method public androidx.heifwriter.HeifWriter build() throws java.io.IOException;
+ method public androidx.heifwriter.HeifWriter.Builder setGridEnabled(boolean);
+ method public androidx.heifwriter.HeifWriter.Builder setHandler(android.os.Handler?);
+ method public androidx.heifwriter.HeifWriter.Builder setMaxImages(@IntRange(from=1) int);
+ method public androidx.heifwriter.HeifWriter.Builder setPrimaryIndex(@IntRange(from=0) int);
+ method public androidx.heifwriter.HeifWriter.Builder setQuality(@IntRange(from=0, to=100) int);
+ method public androidx.heifwriter.HeifWriter.Builder setRotation(@IntRange(from=0) int);
}
}
diff --git a/heifwriter/heifwriter/build.gradle b/heifwriter/heifwriter/build.gradle
index 7bffa1a..3e8ff02 100644
--- a/heifwriter/heifwriter/build.gradle
+++ b/heifwriter/heifwriter/build.gradle
@@ -23,7 +23,7 @@
}
androidx {
- name = "Android Support HeifWriter"
+ name = "HeifWriter"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Android Support HeifWriter for writing HEIF still images"
diff --git a/heifwriter/heifwriter/lint-baseline.xml b/heifwriter/heifwriter/lint-baseline.xml
index bcf9c5b..547bfde 100644
--- a/heifwriter/heifwriter/lint-baseline.xml
+++ b/heifwriter/heifwriter/lint-baseline.xml
@@ -4,6 +4,24 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
+ errorLine1="public final class AvifEncoder extends EncoderBase {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/heifwriter/AvifEncoder.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" public @interface InputMode {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/heifwriter/AvifWriter.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
errorLine1="public class EglRectBlt {"
errorLine2=" ~~~~~~~~~~">
<location
@@ -22,7 +40,16 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1="public final class HeifEncoder implements AutoCloseable,"
+ errorLine1="public class EncoderBase implements AutoCloseable,"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/heifwriter/EncoderBase.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1="public final class HeifEncoder extends EncoderBase {"
errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/androidx/heifwriter/HeifEncoder.java"/>
@@ -56,12 +83,30 @@
</issue>
<issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1="public class WriterBase implements AutoCloseable {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/heifwriter/WriterBase.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" public @interface InputMode {}"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/heifwriter/WriterBase.java"/>
+ </issue>
+
+ <issue
id="BanSynchronizedMethods"
message="Use of synchronized methods is not recommended"
errorLine1=" synchronized void updateInputEOSTime(long timestampNs) {"
errorLine2=" ^">
<location
- file="src/main/java/androidx/heifwriter/HeifEncoder.java"/>
+ file="src/main/java/androidx/heifwriter/EncoderBase.java"/>
</issue>
<issue
@@ -70,7 +115,7 @@
errorLine1=" synchronized boolean updateLastInputAndEncoderTime(long inputTimeNs, long encoderTimeUs) {"
errorLine2=" ^">
<location
- file="src/main/java/androidx/heifwriter/HeifEncoder.java"/>
+ file="src/main/java/androidx/heifwriter/EncoderBase.java"/>
</issue>
<issue
@@ -79,7 +124,7 @@
errorLine1=" synchronized void updateLastOutputTime(long outputTimeUs) {"
errorLine2=" ^">
<location
- file="src/main/java/androidx/heifwriter/HeifEncoder.java"/>
+ file="src/main/java/androidx/heifwriter/EncoderBase.java"/>
</issue>
<issue
@@ -88,7 +133,7 @@
errorLine1=" synchronized void waitForResult(long timeoutMs) throws Exception {"
errorLine2=" ^">
<location
- file="src/main/java/androidx/heifwriter/HeifWriter.java"/>
+ file="src/main/java/androidx/heifwriter/WriterBase.java"/>
</issue>
<issue
@@ -97,7 +142,16 @@
errorLine1=" synchronized void signalResult(@Nullable Exception e) {"
errorLine2=" ^">
<location
- file="src/main/java/androidx/heifwriter/HeifWriter.java"/>
+ file="src/main/java/androidx/heifwriter/WriterBase.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected static String findAv1Fallback() {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/heifwriter/AvifEncoder.java"/>
</issue>
<issue
@@ -166,8 +220,8 @@
<issue
id="UnknownNullness"
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public Surface getSurface() {"
- errorLine2=" ~~~~~~~">
+ errorLine1=" public EglWindowSurface(Surface surface, boolean useHighBitDepth) {"
+ errorLine2=" ~~~~~~~">
<location
file="src/main/java/androidx/heifwriter/EglWindowSurface.java"/>
</issue>
@@ -175,64 +229,10 @@
<issue
id="UnknownNullness"
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public Builder setRotation(int rotation) {"
- errorLine2=" ~~~~~~~">
+ errorLine1=" protected static String findHevcFallback() {"
+ errorLine2=" ~~~~~~">
<location
- file="src/main/java/androidx/heifwriter/HeifWriter.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public Builder setGridEnabled(boolean gridEnabled) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/HeifWriter.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public Builder setQuality(int quality) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/HeifWriter.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public Builder setMaxImages(int maxImages) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/HeifWriter.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public Builder setPrimaryIndex(int primaryIndex) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/HeifWriter.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public Builder setHandler(@Nullable Handler handler) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/HeifWriter.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" public HeifWriter build() throws IOException {"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/HeifWriter.java"/>
+ file="src/main/java/androidx/heifwriter/HeifEncoder.java"/>
</issue>
<issue
diff --git a/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/AvifWriterTest.java b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/AvifWriterTest.java
new file mode 100644
index 0000000..ee27431
--- /dev/null
+++ b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/AvifWriterTest.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 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.heifwriter;
+
+import static androidx.heifwriter.AvifWriter.INPUT_MODE_BITMAP;
+import static androidx.heifwriter.AvifWriter.INPUT_MODE_BUFFER;
+import static androidx.heifwriter.AvifWriter.INPUT_MODE_SURFACE;
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.Manifest;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.ImageFormat;
+import android.media.MediaFormat;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.util.Log;
+
+import androidx.heifwriter.test.R;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.GrantPermissionRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Test {@link AvifWriter}.
+ */
+@RunWith(AndroidJUnit4.class)
+@FlakyTest
+public class AvifWriterTest extends TestBase {
+ private static final String TAG = AvifWriterTest.class.getSimpleName();
+
+ @Rule
+ public GrantPermissionRule mRuntimePermissionRule1 =
+ GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE);
+
+ @Rule
+ public GrantPermissionRule mRuntimePermissionRule =
+ GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE);
+
+ private static final boolean DEBUG = true;
+ private static final boolean DUMP_YUV_INPUT = false;
+
+ private static final String AVIFWRITER_INPUT = "heifwriter_input.heic";
+ private static final int[] IMAGE_RESOURCES = new int[] {
+ R.raw.heifwriter_input
+ };
+ private static final String[] IMAGE_FILENAMES = new String[] {
+ AVIFWRITER_INPUT
+ };
+ private static final String OUTPUT_FILENAME = "output.avif";
+
+ @Before
+ public void setUp() throws Exception {
+ for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+ String outputPath = new File(getApplicationContext().getExternalFilesDir(null),
+ IMAGE_FILENAMES[i]).getAbsolutePath();
+
+ InputStream inputStream = null;
+ FileOutputStream outputStream = null;
+ try {
+ inputStream = getApplicationContext()
+ .getResources().openRawResource(IMAGE_RESOURCES[i]);
+ outputStream = new FileOutputStream(outputPath);
+ copy(inputStream, outputStream);
+ } finally {
+ closeQuietly(inputStream);
+ closeQuietly(outputStream);
+ }
+ }
+
+ HandlerThread handlerThread = new HandlerThread(
+ "AvifEncoderThread", Process.THREAD_PRIORITY_FOREGROUND);
+ handlerThread.start();
+ mHandler = new Handler(handlerThread.getLooper());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+ String imageFilePath = new File(getApplicationContext().getExternalFilesDir(null),
+ IMAGE_FILENAMES[i]).getAbsolutePath();
+ File imageFile = new File(imageFilePath);
+ if (imageFile.exists()) {
+ imageFile.delete();
+ }
+ }
+ }
+
+ @Test
+ @LargeTest
+ public void testInputBuffer_NoGrid_NoHandler() throws Throwable {
+ if (shouldSkip()) return;
+
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_BUFFER, false, false, OUTPUT_FILENAME);
+ doTestForVariousNumberImages(builder);
+ }
+
+ @Test
+ @LargeTest
+ public void testInputBuffer_Grid_NoHandler() throws Throwable {
+ if (shouldSkip()) return;
+
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_BUFFER, true, false, OUTPUT_FILENAME);
+ doTestForVariousNumberImages(builder);
+ }
+
+ @Test
+ @LargeTest
+ public void testInputBuffer_NoGrid_Handler() throws Throwable {
+ if (shouldSkip()) return;
+
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_BUFFER, false, true, OUTPUT_FILENAME);
+ doTestForVariousNumberImages(builder);
+ }
+
+ @Test
+ @LargeTest
+ public void testInputBuffer_Grid_Handler() throws Throwable {
+ if (shouldSkip()) return;
+
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_BUFFER, true, true, OUTPUT_FILENAME);
+ doTestForVariousNumberImages(builder);
+ }
+
+ @SdkSuppress(maxSdkVersion = 29) // b/192261638
+ @Test
+ @LargeTest
+ public void testInputSurface_NoGrid_NoHandler() throws Throwable {
+ if (shouldSkip()) return;
+
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_SURFACE, false, false, OUTPUT_FILENAME);
+ doTestForVariousNumberImages(builder);
+ }
+ //
+ @SdkSuppress(maxSdkVersion = 29) // b/192261638
+ @Test
+ @LargeTest
+ public void testInputSurface_Grid_NoHandler() throws Throwable {
+ if (shouldSkip()) return;
+
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_SURFACE, true, false, OUTPUT_FILENAME);
+ doTestForVariousNumberImages(builder);
+ }
+
+ @SdkSuppress(maxSdkVersion = 29) // b/192261638
+ @Test
+ @LargeTest
+ public void testInputSurface_NoGrid_Handler() throws Throwable {
+ if (shouldSkip()) return;
+
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_SURFACE, false, true, OUTPUT_FILENAME);
+ doTestForVariousNumberImages(builder);
+ }
+
+ @SdkSuppress(maxSdkVersion = 29) // b/192261638
+ @Test
+ @LargeTest
+ public void testInputSurface_Grid_Handler() throws Throwable {
+ if (shouldSkip()) return;
+
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_SURFACE, true, true, OUTPUT_FILENAME);
+ doTestForVariousNumberImages(builder);
+ }
+
+
+ @Test
+ @LargeTest
+ public void testInputBitmap_NoGrid_NoHandler() throws Throwable {
+ if (shouldSkip()) return;
+
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_BITMAP, false, false, OUTPUT_FILENAME);
+ for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+ String inputPath = new File(getApplicationContext().getExternalFilesDir(null),
+ IMAGE_FILENAMES[i]).getAbsolutePath();
+ doTestForVariousNumberImages(builder.setInputPath(inputPath));
+ }
+ }
+
+ @SdkSuppress(maxSdkVersion = 29) // b/192261638
+ @Test
+ @LargeTest
+ public void testInputBitmap_Grid_NoHandler() throws Throwable {
+ if (shouldSkip()) return;
+
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_BITMAP, true, false, OUTPUT_FILENAME);
+ for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+ String inputPath = new File(getApplicationContext().getExternalFilesDir(null),
+ IMAGE_FILENAMES[i]).getAbsolutePath();
+ doTestForVariousNumberImages(builder.setInputPath(inputPath));
+ }
+ }
+
+ @SdkSuppress(maxSdkVersion = 29) // b/192261638
+ @Test
+ @LargeTest
+ public void testInputBitmap_NoGrid_Handler() throws Throwable {
+ if (shouldSkip()) return;
+
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_BITMAP, false, true, OUTPUT_FILENAME);
+ for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+ String inputPath = new File(getApplicationContext().getExternalFilesDir(null),
+ IMAGE_FILENAMES[i]).getAbsolutePath();
+ doTestForVariousNumberImages(builder.setInputPath(inputPath));
+ }
+ }
+
+ @SdkSuppress(maxSdkVersion = 29) // b/192261638
+ @Test
+ @LargeTest
+ public void testInputBitmap_Grid_Handler() throws Throwable {
+ if (shouldSkip()) return;
+
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_BITMAP, true, true, OUTPUT_FILENAME);
+ for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+ String inputPath = new File(getApplicationContext().getExternalFilesDir(null),
+ IMAGE_FILENAMES[i]).getAbsolutePath();
+ doTestForVariousNumberImages(builder.setInputPath(inputPath));
+ }
+ }
+
+ @SdkSuppress(maxSdkVersion = 29) // b/192261638
+ @Test
+ @SmallTest
+ public void testCloseWithoutStart() throws Throwable {
+ if (shouldSkip()) return;
+
+ final String outputPath = new File(getApplicationContext().getExternalFilesDir(null),
+ OUTPUT_FILENAME).getAbsolutePath();
+ AvifWriter avifWriter = new AvifWriter.Builder(
+ outputPath, 1920, 1080, INPUT_MODE_SURFACE)
+ .setGridEnabled(true)
+ .setMaxImages(4)
+ .setQuality(90)
+ .setPrimaryIndex(0)
+ .setHandler(mHandler)
+ .build();
+
+ avifWriter.close();
+ }
+
+ private void doTestForVariousNumberImages(TestConfig.Builder builder) throws Exception {
+ builder.setHighBitDepthEnabled(false);
+ builder.setNumImages(4);
+ doTest(builder.setRotation(270).build());
+ doTest(builder.setRotation(180).build());
+ doTest(builder.setRotation(90).build());
+ doTest(builder.setRotation(0).build());
+ doTest(builder.setNumImages(1).build());
+ doTest(builder.setNumImages(8).build());
+
+ builder.setHighBitDepthEnabled(true);
+ builder.setNumImages(1);
+ doTest(builder.setRotation(270).build());
+ doTest(builder.setRotation(180).build());
+ doTest(builder.setRotation(90).build());
+ doTest(builder.setRotation(0).build());
+ doTest(builder.setNumImages(1).build());
+ doTest(builder.setNumImages(8).build());
+ }
+
+ private boolean shouldSkip() {
+ return !hasEncoderForMime(MediaFormat.MIMETYPE_VIDEO_AV1);
+ }
+
+ private static byte[] mYuvData;
+ private void doTest(final TestConfig config) throws Exception {
+ final int width = config.mWidth;
+ final int height = config.mHeight;
+ final int actualNumImages = config.mActualNumImages;
+
+ mInputIndex = 0;
+ AvifWriter avifWriter = null;
+ FileInputStream inputStream = null;
+ FileOutputStream outputStream = null;
+ String outputFileName;
+ try {
+ if (DEBUG)
+ Log.d(TAG, "started: " + config);
+ outputFileName = new File(getApplicationContext().getExternalFilesDir(null),
+ OUTPUT_FILENAME).getAbsolutePath();
+
+ if(!config.mUseHighBitDepth){
+ avifWriter =
+ new AvifWriter.Builder(outputFileName, width, height, config.mInputMode)
+ .setRotation(config.mRotation)
+ .setGridEnabled(config.mUseGrid)
+ .setMaxImages(config.mMaxNumImages)
+ .setQuality(config.mQuality)
+ .setPrimaryIndex(config.mMaxNumImages - 1)
+ .setHandler(config.mUseHandler ? mHandler : null)
+ .build();
+ } else {
+ avifWriter =
+ new AvifWriter.Builder(outputFileName, width, height, config.mInputMode)
+ .setRotation(config.mRotation)
+ .setGridEnabled(config.mUseGrid)
+ .setMaxImages(config.mMaxNumImages)
+ .setQuality(config.mQuality)
+ .setPrimaryIndex(config.mMaxNumImages - 1)
+ .setHandler(config.mUseHandler ? mHandler : null)
+ .setHighBitDepthEnabled(true)
+ .build();
+ }
+
+ if (config.mInputMode == INPUT_MODE_SURFACE) {
+ mInputEglSurface = new EglWindowSurface(avifWriter.getInputSurface());
+ }
+
+ avifWriter.start();
+
+ if (config.mInputMode == INPUT_MODE_BUFFER) {
+ if (!config.mUseHighBitDepth) {
+ if (mYuvData == null || mYuvData.length != width * height * 3 / 2) {
+ mYuvData = new byte[width * height * 3 / 2];
+ }
+ } else {
+ if (mYuvData == null || mYuvData.length != width * height * 3) {
+ mYuvData = new byte[width * height * 3];
+ }
+ }
+
+ if (config.mInputPath != null) {
+ inputStream = new FileInputStream(config.mInputPath);
+ }
+
+ if (DUMP_YUV_INPUT) {
+ File outputFile = new File("/sdcard/input.yuv");
+ outputFile.createNewFile();
+ outputStream = new FileOutputStream(outputFile);
+ }
+
+ for (int i = 0; i < actualNumImages; i++) {
+ if (DEBUG)
+ Log.d(TAG, "fillYuvBuffer: " + i);
+ fillYuvBuffer(i, mYuvData, width, height, inputStream);
+ if (DUMP_YUV_INPUT) {
+ Log.d(TAG, "@@@ dumping input YUV");
+ outputStream.write(mYuvData);
+ }
+ if (!config.mUseHighBitDepth) {
+ avifWriter.addYuvBuffer(ImageFormat.YUV_420_888, mYuvData);
+ } else {
+ avifWriter.addYuvBuffer(ImageFormat.YCBCR_P010, mYuvData);
+ }
+ }
+ } else if (config.mInputMode == INPUT_MODE_SURFACE) {
+ // The input surface is a surface texture using single buffer mode, draws will be
+ // blocked until onFrameAvailable is done with the buffer, which is dependant on
+ // how fast MediaCodec processes them, which is further dependent on how fast the
+ // MediaCodec callbacks are handled. We can't put draws on the same looper that
+ // handles MediaCodec callback, it will cause deadlock.
+ for (int i = 0; i < actualNumImages; i++) {
+ if (DEBUG)
+ Log.d(TAG, "drawFrame: " + i);
+ drawFrame(width, height);
+ }
+ avifWriter.setInputEndOfStreamTimestamp(
+ 1000 * computePresentationTime(actualNumImages - 1));
+ } else if (config.mInputMode == INPUT_MODE_BITMAP) {
+ if(!config.mUseHighBitDepth) {
+ Bitmap[] bitmaps = config.mBitmaps;
+ for (int i = 0; i < Math.min(bitmaps.length, actualNumImages); i++) {
+ if (DEBUG) {
+ Log.d(TAG, "addBitmap: " + i);
+ }
+ avifWriter.addBitmap(bitmaps[i]);
+ bitmaps[i].recycle();
+ }
+ } else {
+ BitmapFactory.Options opt = new BitmapFactory.Options();
+ opt.inPreferredConfig = Bitmap.Config.RGBA_F16;
+ InputStream inputStream10Bit = getApplicationContext().getResources()
+ .openRawResource(R.raw.heifwriter_input10);
+ Bitmap bm = BitmapFactory.decodeStream(inputStream10Bit, null, opt);
+ assertNotNull(bm);
+ avifWriter.addBitmap(bm);
+ bm.recycle();
+ }
+ }
+
+ avifWriter.stop(10000);
+ // The test sets the primary index to the last image.
+ // However, if we're testing early abort, the last image will not be
+ // present and the muxer is supposed to set it to 0 by default.
+ int expectedPrimary = config.mMaxNumImages - 1;
+ int expectedImageCount = config.mMaxNumImages;
+ if (actualNumImages < config.mMaxNumImages) {
+ expectedPrimary = 0;
+ expectedImageCount = actualNumImages;
+ }
+ verifyResult(config.mOutputPath, width, height, config.mRotation,
+ expectedImageCount, expectedPrimary, config.mUseGrid,
+ config.mInputMode == INPUT_MODE_SURFACE);
+ if (DEBUG)
+ Log.d(TAG, "finished: PASS");
+ } finally {
+ try {
+ if (outputStream != null) {
+ outputStream.close();
+ }
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (IOException e) {
+ }
+
+ if (avifWriter != null) {
+ avifWriter.close();
+ avifWriter = null;
+ }
+ if (mInputEglSurface != null) {
+ // This also releases the surface from encoder.
+ mInputEglSurface.release();
+ mInputEglSurface = null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
index b8e3752..a536c7f 100644
--- a/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
+++ b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
@@ -21,28 +21,15 @@
import static androidx.heifwriter.HeifWriter.INPUT_MODE_SURFACE;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
import android.Manifest;
import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Color;
import android.graphics.ImageFormat;
-import android.graphics.Rect;
-import android.media.MediaCodecInfo;
-import android.media.MediaCodecList;
-import android.media.MediaExtractor;
import android.media.MediaFormat;
-import android.media.MediaMetadataRetriever;
-import android.opengl.GLES20;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.heifwriter.test.R;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.FlakyTest;
@@ -58,80 +45,52 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.util.Arrays;
/**
* Test {@link HeifWriter}.
*/
@RunWith(AndroidJUnit4.class)
@FlakyTest
-public class HeifWriterTest {
+public class HeifWriterTest extends TestBase {
private static final String TAG = HeifWriterTest.class.getSimpleName();
- private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
-
@Rule
public GrantPermissionRule mRuntimePermissionRule1 =
- GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE);
+ GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE);
@Rule
public GrantPermissionRule mRuntimePermissionRule =
- GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE);
+ GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE);
- private static final boolean DEBUG = false;
+ private static final boolean DEBUG = true;
private static final boolean DUMP_YUV_INPUT = false;
- private static final byte[][] TEST_YUV_COLORS = {
- {(byte) 255, (byte) 0, (byte) 0},
- {(byte) 255, (byte) 0, (byte) 255},
- {(byte) 255, (byte) 255, (byte) 255},
- {(byte) 255, (byte) 255, (byte) 0},
- };
- private static final Color COLOR_BLOCK =
- Color.valueOf(1.0f, 1.0f, 1.0f);
- private static final Color[] COLOR_BARS = {
- Color.valueOf(0.0f, 0.0f, 0.0f),
- Color.valueOf(0.0f, 0.0f, 0.64f),
- Color.valueOf(0.0f, 0.64f, 0.0f),
- Color.valueOf(0.0f, 0.64f, 0.64f),
- Color.valueOf(0.64f, 0.0f, 0.0f),
- Color.valueOf(0.64f, 0.0f, 0.64f),
- Color.valueOf(0.64f, 0.64f, 0.0f),
- };
- private static final float MAX_DELTA = 0.025f;
- private static final int BORDER_WIDTH = 16;
-
private static final String HEIFWRITER_INPUT = "heifwriter_input.heic";
private static final int[] IMAGE_RESOURCES = new int[] {
- R.raw.heifwriter_input
+ R.raw.heifwriter_input
};
private static final String[] IMAGE_FILENAMES = new String[] {
- HEIFWRITER_INPUT
+ HEIFWRITER_INPUT
};
private static final String OUTPUT_FILENAME = "output.heic";
- private EglWindowSurface mInputEglSurface;
- private Handler mHandler;
- private int mInputIndex;
-
@Before
public void setUp() throws Exception {
for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
String outputPath = new File(getApplicationContext().getExternalFilesDir(null),
- IMAGE_FILENAMES[i]).getAbsolutePath();
+ IMAGE_FILENAMES[i]).getAbsolutePath();
InputStream inputStream = null;
FileOutputStream outputStream = null;
try {
inputStream = getApplicationContext()
- .getResources().openRawResource(IMAGE_RESOURCES[i]);
+ .getResources().openRawResource(IMAGE_RESOURCES[i]);
outputStream = new FileOutputStream(outputPath);
copy(inputStream, outputStream);
} finally {
@@ -141,7 +100,7 @@
}
HandlerThread handlerThread = new HandlerThread(
- "HeifEncoderThread", Process.THREAD_PRIORITY_FOREGROUND);
+ "HeifEncoderThread", Process.THREAD_PRIORITY_FOREGROUND);
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper());
}
@@ -150,7 +109,7 @@
public void tearDown() throws Exception {
for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
String imageFilePath = new File(getApplicationContext().getExternalFilesDir(null),
- IMAGE_FILENAMES[i]).getAbsolutePath();
+ IMAGE_FILENAMES[i]).getAbsolutePath();
File imageFile = new File(imageFilePath);
if (imageFile.exists()) {
imageFile.delete();
@@ -164,7 +123,8 @@
public void testInputBuffer_NoGrid_NoHandler() throws Throwable {
if (shouldSkip()) return;
- TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BUFFER, false, false);
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_BUFFER, false, false, OUTPUT_FILENAME);
doTestForVariousNumberImages(builder);
}
@@ -174,7 +134,8 @@
public void testInputBuffer_Grid_NoHandler() throws Throwable {
if (shouldSkip()) return;
- TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BUFFER, true, false);
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_BUFFER, true, false, OUTPUT_FILENAME);
doTestForVariousNumberImages(builder);
}
@@ -184,7 +145,8 @@
public void testInputBuffer_NoGrid_Handler() throws Throwable {
if (shouldSkip()) return;
- TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BUFFER, false, true);
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_BUFFER, false, true, OUTPUT_FILENAME);
doTestForVariousNumberImages(builder);
}
@@ -194,7 +156,8 @@
public void testInputBuffer_Grid_Handler() throws Throwable {
if (shouldSkip()) return;
- TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BUFFER, true, true);
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_BUFFER, true, true, OUTPUT_FILENAME);
doTestForVariousNumberImages(builder);
}
@@ -204,17 +167,19 @@
public void testInputSurface_NoGrid_NoHandler() throws Throwable {
if (shouldSkip()) return;
- TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_SURFACE, false, false);
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_SURFACE, false, false, OUTPUT_FILENAME);
doTestForVariousNumberImages(builder);
}
-
+ //
@SdkSuppress(maxSdkVersion = 29) // b/192261638
@Test
@LargeTest
public void testInputSurface_Grid_NoHandler() throws Throwable {
if (shouldSkip()) return;
- TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_SURFACE, true, false);
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_SURFACE, true, false, OUTPUT_FILENAME);
doTestForVariousNumberImages(builder);
}
@@ -224,7 +189,8 @@
public void testInputSurface_NoGrid_Handler() throws Throwable {
if (shouldSkip()) return;
- TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_SURFACE, false, true);
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_SURFACE, false, true, OUTPUT_FILENAME);
doTestForVariousNumberImages(builder);
}
@@ -234,20 +200,23 @@
public void testInputSurface_Grid_Handler() throws Throwable {
if (shouldSkip()) return;
- TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_SURFACE, true, true);
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_SURFACE, true, true, OUTPUT_FILENAME);
doTestForVariousNumberImages(builder);
}
+
@SdkSuppress(maxSdkVersion = 29) // b/192261638
@Test
@LargeTest
public void testInputBitmap_NoGrid_NoHandler() throws Throwable {
if (shouldSkip()) return;
- TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BITMAP, false, false);
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_BITMAP, false, false, OUTPUT_FILENAME);
for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
String inputPath = new File(getApplicationContext().getExternalFilesDir(null),
- IMAGE_FILENAMES[i]).getAbsolutePath();
+ IMAGE_FILENAMES[i]).getAbsolutePath();
doTestForVariousNumberImages(builder.setInputPath(inputPath));
}
}
@@ -258,10 +227,11 @@
public void testInputBitmap_Grid_NoHandler() throws Throwable {
if (shouldSkip()) return;
- TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BITMAP, true, false);
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_BITMAP, true, false, OUTPUT_FILENAME);
for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
String inputPath = new File(getApplicationContext().getExternalFilesDir(null),
- IMAGE_FILENAMES[i]).getAbsolutePath();
+ IMAGE_FILENAMES[i]).getAbsolutePath();
doTestForVariousNumberImages(builder.setInputPath(inputPath));
}
}
@@ -272,10 +242,11 @@
public void testInputBitmap_NoGrid_Handler() throws Throwable {
if (shouldSkip()) return;
- TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BITMAP, false, true);
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_BITMAP, false, true, OUTPUT_FILENAME);
for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
String inputPath = new File(getApplicationContext().getExternalFilesDir(null),
- IMAGE_FILENAMES[i]).getAbsolutePath();
+ IMAGE_FILENAMES[i]).getAbsolutePath();
doTestForVariousNumberImages(builder.setInputPath(inputPath));
}
}
@@ -286,10 +257,11 @@
public void testInputBitmap_Grid_Handler() throws Throwable {
if (shouldSkip()) return;
- TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BITMAP, true, true);
+ TestConfig.Builder builder =
+ new TestConfig.Builder(INPUT_MODE_BITMAP, true, true, OUTPUT_FILENAME);
for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
String inputPath = new File(getApplicationContext().getExternalFilesDir(null),
- IMAGE_FILENAMES[i]).getAbsolutePath();
+ IMAGE_FILENAMES[i]).getAbsolutePath();
doTestForVariousNumberImages(builder.setInputPath(inputPath));
}
}
@@ -301,15 +273,15 @@
if (shouldSkip()) return;
final String outputPath = new File(getApplicationContext().getExternalFilesDir(null),
- OUTPUT_FILENAME).getAbsolutePath();
+ OUTPUT_FILENAME).getAbsolutePath();
HeifWriter heifWriter = new HeifWriter.Builder(
- outputPath, 1920, 1080, INPUT_MODE_SURFACE)
- .setGridEnabled(true)
- .setMaxImages(4)
- .setQuality(90)
- .setPrimaryIndex(0)
- .setHandler(mHandler)
- .build();
+ outputPath, 1920, 1080, INPUT_MODE_SURFACE)
+ .setGridEnabled(true)
+ .setMaxImages(4)
+ .setQuality(90)
+ .setPrimaryIndex(0)
+ .setHandler(mHandler)
+ .build();
heifWriter.close();
}
@@ -324,186 +296,11 @@
doTest(builder.setNumImages(8).build());
}
- private void closeQuietly(Closeable closeable) {
- if (closeable != null) {
- try {
- closeable.close();
- } catch (RuntimeException rethrown) {
- throw rethrown;
- } catch (Exception ignored) {
- }
- }
- }
-
- private int copy(InputStream in, OutputStream out) throws IOException {
- int total = 0;
- byte[] buffer = new byte[8192];
- int c;
- while ((c = in.read(buffer)) != -1) {
- total += c;
- out.write(buffer, 0, c);
- }
- return total;
- }
-
private boolean shouldSkip() {
return !hasEncoderForMime(MediaFormat.MIMETYPE_VIDEO_HEVC)
&& !hasEncoderForMime(MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
}
- private boolean hasEncoderForMime(String mime) {
- for (MediaCodecInfo info : sMCL.getCodecInfos()) {
- if (info.isEncoder()) {
- for (String type : info.getSupportedTypes()) {
- if (type.equalsIgnoreCase(mime)) {
- Log.i(TAG, "found codec " + info.getName() + " for mime " + mime);
- return true;
- }
- }
- }
- }
- return false;
- }
-
- private static class TestConfig {
- final int mInputMode;
- final boolean mUseGrid;
- final boolean mUseHandler;
- final int mMaxNumImages;
- final int mActualNumImages;
- final int mWidth;
- final int mHeight;
- final int mRotation;
- final int mQuality;
- final String mInputPath;
- final String mOutputPath;
- final Bitmap[] mBitmaps;
-
- TestConfig(int inputMode, boolean useGrid, boolean useHandler,
- int maxNumImages, int actualNumImages, int width, int height,
- int rotation, int quality,
- String inputPath, String outputPath, Bitmap[] bitmaps) {
- mInputMode = inputMode;
- mUseGrid = useGrid;
- mUseHandler = useHandler;
- mMaxNumImages = maxNumImages;
- mActualNumImages = actualNumImages;
- mWidth = width;
- mHeight = height;
- mRotation = rotation;
- mQuality = quality;
- mInputPath = inputPath;
- mOutputPath = outputPath;
- mBitmaps = bitmaps;
- }
-
- static class Builder {
- final int mInputMode;
- final boolean mUseGrid;
- final boolean mUseHandler;
- int mMaxNumImages;
- int mNumImages;
- int mWidth;
- int mHeight;
- int mRotation;
- final int mQuality;
- String mInputPath;
- final String mOutputPath;
- Bitmap[] mBitmaps;
- boolean mNumImagesSetExplicitly;
-
-
- Builder(int inputMode, boolean useGrids, boolean useHandler) {
- mInputMode = inputMode;
- mUseGrid = useGrids;
- mUseHandler = useHandler;
- mMaxNumImages = mNumImages = 4;
- mWidth = 1920;
- mHeight = 1080;
- mRotation = 0;
- mQuality = 100;
- mOutputPath = new File(getApplicationContext().getExternalFilesDir(null),
- OUTPUT_FILENAME).getAbsolutePath();
- }
-
- Builder setInputPath(String inputPath) {
- mInputPath = (mInputMode == INPUT_MODE_BITMAP) ? inputPath : null;
- return this;
- }
-
- Builder setNumImages(int numImages) {
- mNumImagesSetExplicitly = true;
- mNumImages = numImages;
- return this;
- }
-
- Builder setRotation(int rotation) {
- mRotation = rotation;
- return this;
- }
-
- private void loadBitmapInputs() {
- if (mInputMode != INPUT_MODE_BITMAP) {
- return;
- }
- MediaMetadataRetriever retriever = new MediaMetadataRetriever();
- retriever.setDataSource(mInputPath);
- String hasImage = retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
- if (!"yes".equals(hasImage)) {
- throw new IllegalArgumentException("no bitmap found!");
- }
- mMaxNumImages = Math.min(mMaxNumImages, Integer.parseInt(retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
- if (!mNumImagesSetExplicitly) {
- mNumImages = mMaxNumImages;
- }
- mBitmaps = new Bitmap[mMaxNumImages];
- for (int i = 0; i < mBitmaps.length; i++) {
- mBitmaps[i] = retriever.getImageAtIndex(i);
- }
- mWidth = mBitmaps[0].getWidth();
- mHeight = mBitmaps[0].getHeight();
- try {
- retriever.release();
- } catch (IOException e) {
- // Nothing we can do about it.
- }
- }
-
- private void cleanupStaleOutputs() {
- File outputFile = new File(mOutputPath);
- if (outputFile.exists()) {
- outputFile.delete();
- }
- }
-
- TestConfig build() {
- cleanupStaleOutputs();
- loadBitmapInputs();
-
- return new TestConfig(mInputMode, mUseGrid, mUseHandler, mMaxNumImages, mNumImages,
- mWidth, mHeight, mRotation, mQuality, mInputPath, mOutputPath, mBitmaps);
- }
- }
-
- @Override
- public String toString() {
- return "TestConfig"
- + ": mInputMode " + mInputMode
- + ", mUseGrid " + mUseGrid
- + ", mUseHandler " + mUseHandler
- + ", mMaxNumImages " + mMaxNumImages
- + ", mNumImages " + mActualNumImages
- + ", mWidth " + mWidth
- + ", mHeight " + mHeight
- + ", mRotation " + mRotation
- + ", mQuality " + mQuality
- + ", mInputPath " + mInputPath
- + ", mOutputPath " + mOutputPath;
- }
- }
-
private static byte[] mYuvData;
private void doTest(final TestConfig config) throws Exception {
final int width = config.mWidth;
@@ -515,17 +312,19 @@
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
try {
- if (DEBUG) Log.d(TAG, "started: " + config);
+ if (DEBUG)
+ Log.d(TAG, "started: " + config);
heifWriter = new HeifWriter.Builder(
- config.mOutputPath, width, height, config.mInputMode)
- .setRotation(config.mRotation)
- .setGridEnabled(config.mUseGrid)
- .setMaxImages(config.mMaxNumImages)
- .setQuality(config.mQuality)
- .setPrimaryIndex(config.mMaxNumImages - 1)
- .setHandler(config.mUseHandler ? mHandler : null)
- .build();
+ new File(getApplicationContext().getExternalFilesDir(null),
+ OUTPUT_FILENAME).getAbsolutePath(), width, height, config.mInputMode)
+ .setRotation(config.mRotation)
+ .setGridEnabled(config.mUseGrid)
+ .setMaxImages(config.mMaxNumImages)
+ .setQuality(config.mQuality)
+ .setPrimaryIndex(config.mMaxNumImages - 1)
+ .setHandler(config.mUseHandler ? mHandler : null)
+ .build();
if (config.mInputMode == INPUT_MODE_SURFACE) {
mInputEglSurface = new EglWindowSurface(heifWriter.getInputSurface());
@@ -549,7 +348,8 @@
}
for (int i = 0; i < actualNumImages; i++) {
- if (DEBUG) Log.d(TAG, "fillYuvBuffer: " + i);
+ if (DEBUG)
+ Log.d(TAG, "fillYuvBuffer: " + i);
fillYuvBuffer(i, mYuvData, width, height, inputStream);
if (DUMP_YUV_INPUT) {
Log.d(TAG, "@@@ dumping input YUV");
@@ -564,15 +364,17 @@
// MediaCodec callbacks are handled. We can't put draws on the same looper that
// handles MediaCodec callback, it will cause deadlock.
for (int i = 0; i < actualNumImages; i++) {
- if (DEBUG) Log.d(TAG, "drawFrame: " + i);
+ if (DEBUG)
+ Log.d(TAG, "drawFrame: " + i);
drawFrame(width, height);
}
heifWriter.setInputEndOfStreamTimestamp(
- 1000 * computePresentationTime(actualNumImages - 1));
+ 1000 * computePresentationTime(actualNumImages - 1));
} else if (config.mInputMode == INPUT_MODE_BITMAP) {
Bitmap[] bitmaps = config.mBitmaps;
for (int i = 0; i < Math.min(bitmaps.length, actualNumImages); i++) {
- if (DEBUG) Log.d(TAG, "addBitmap: " + i);
+ if (DEBUG)
+ Log.d(TAG, "addBitmap: " + i);
heifWriter.addBitmap(bitmaps[i]);
bitmaps[i].recycle();
}
@@ -589,9 +391,10 @@
expectedImageCount = actualNumImages;
}
verifyResult(config.mOutputPath, width, height, config.mRotation,
- expectedImageCount, expectedPrimary, config.mUseGrid,
- config.mInputMode == INPUT_MODE_SURFACE);
- if (DEBUG) Log.d(TAG, "finished: PASS");
+ expectedImageCount, expectedPrimary, config.mUseGrid,
+ config.mInputMode == INPUT_MODE_SURFACE);
+ if (DEBUG)
+ Log.d(TAG, "finished: PASS");
} finally {
try {
if (outputStream != null) {
@@ -600,7 +403,8 @@
if (inputStream != null) {
inputStream.close();
}
- } catch (IOException e) {}
+ } catch (IOException e) {
+ }
if (heifWriter != null) {
heifWriter.close();
@@ -613,139 +417,4 @@
}
}
}
-
- private long computePresentationTime(int frameIndex) {
- return 132 + (long)frameIndex * 1000000;
- }
-
- private void fillYuvBuffer(int frameIndex, @NonNull byte[] data, int width, int height,
- @Nullable FileInputStream inputStream) throws IOException {
- if (inputStream != null) {
- inputStream.read(data);
- } else {
- byte[] color = TEST_YUV_COLORS[frameIndex % TEST_YUV_COLORS.length];
- int sizeY = width * height;
- Arrays.fill(data, 0, sizeY, color[0]);
- Arrays.fill(data, sizeY, sizeY * 5 / 4, color[1]);
- Arrays.fill(data, sizeY * 5 / 4, sizeY * 3 / 2, color[2]);
- }
- }
-
- private void drawFrame(int width, int height) {
- mInputEglSurface.makeCurrent();
- generateSurfaceFrame(mInputIndex, width, height);
- mInputEglSurface.setPresentationTime(1000 * computePresentationTime(mInputIndex));
- mInputEglSurface.swapBuffers();
- mInputIndex++;
- }
-
- private static Rect getColorBarRect(int index, int width, int height) {
- int barWidth = (width - BORDER_WIDTH * 2) / COLOR_BARS.length;
- return new Rect(BORDER_WIDTH + barWidth * index, BORDER_WIDTH,
- BORDER_WIDTH + barWidth * (index + 1), height - BORDER_WIDTH);
- }
-
- private static Rect getColorBlockRect(int index, int width, int height) {
- int blockCenterX = (width / 5) * (index % 4 + 1);
- return new Rect(blockCenterX - width / 10, height / 6,
- blockCenterX + width / 10, height / 3);
- }
-
- private void generateSurfaceFrame(int frameIndex, int width, int height) {
- GLES20.glViewport(0, 0, width, height);
- GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
- GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
- GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
-
- for (int i = 0; i < COLOR_BARS.length; i++) {
- Rect r = getColorBarRect(i, width, height);
-
- GLES20.glScissor(r.left, r.top, r.width(), r.height());
- final Color color = COLOR_BARS[i];
- GLES20.glClearColor(color.red(), color.green(), color.blue(), 1.0f);
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
- }
-
- Rect r = getColorBlockRect(frameIndex, width, height);
- GLES20.glScissor(r.left, r.top, r.width(), r.height());
- GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
- r.inset(BORDER_WIDTH, BORDER_WIDTH);
- GLES20.glScissor(r.left, r.top, r.width(), r.height());
- GLES20.glClearColor(COLOR_BLOCK.red(), COLOR_BLOCK.green(), COLOR_BLOCK.blue(), 1.0f);
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
- }
-
- /**
- * Determines if two color values are approximately equal.
- */
- private static boolean approxEquals(Color expected, Color actual) {
- return (Math.abs(expected.red() - actual.red()) <= MAX_DELTA)
- && (Math.abs(expected.green() - actual.green()) <= MAX_DELTA)
- && (Math.abs(expected.blue() - actual.blue()) <= MAX_DELTA);
- }
-
- private void verifyResult(
- String filename, int width, int height, int rotation,
- int imageCount, int primary, boolean useGrid, boolean checkColor)
- throws Exception {
- MediaMetadataRetriever retriever = new MediaMetadataRetriever();
- retriever.setDataSource(filename);
- String hasImage = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
- if (!"yes".equals(hasImage)) {
- throw new Exception("No images found in file " + filename);
- }
- assertEquals("Wrong width", width,
- Integer.parseInt(retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH)));
- assertEquals("Wrong height", height,
- Integer.parseInt(retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)));
- assertEquals("Wrong rotation", rotation,
- Integer.parseInt(retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION)));
- assertEquals("Wrong image count", imageCount,
- Integer.parseInt(retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
- assertEquals("Wrong primary index", primary,
- Integer.parseInt(retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_IMAGE_PRIMARY)));
- try {
- retriever.release();
- } catch (IOException e) {
- // Nothing we can do about it.
- }
-
- if (useGrid) {
- MediaExtractor extractor = new MediaExtractor();
- extractor.setDataSource(filename);
- MediaFormat format = extractor.getTrackFormat(0);
- int tileWidth = format.getInteger(MediaFormat.KEY_TILE_WIDTH);
- int tileHeight = format.getInteger(MediaFormat.KEY_TILE_HEIGHT);
- int gridRows = format.getInteger(MediaFormat.KEY_GRID_ROWS);
- int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLUMNS);
- assertTrue("Wrong tile width or grid cols",
- ((width + tileWidth - 1) / tileWidth) == gridCols);
- assertTrue("Wrong tile height or grid rows",
- ((height + tileHeight - 1) / tileHeight) == gridRows);
- extractor.release();
- }
-
- if (checkColor) {
- Bitmap bitmap = BitmapFactory.decodeFile(filename);
-
- for (int i = 0; i < COLOR_BARS.length; i++) {
- Rect r = getColorBarRect(i, width, height);
- assertTrue("Color bar " + i + " doesn't match", approxEquals(COLOR_BARS[i],
- Color.valueOf(bitmap.getPixel(r.centerX(), r.centerY()))));
- }
-
- Rect r = getColorBlockRect(primary, width, height);
- assertTrue("Color block doesn't match", approxEquals(COLOR_BLOCK,
- Color.valueOf(bitmap.getPixel(r.centerX(), height - r.centerY()))));
-
- bitmap.recycle();
- }
- }
-}
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/TestBase.java b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/TestBase.java
new file mode 100644
index 0000000..39be1ea
--- /dev/null
+++ b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/TestBase.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 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.heifwriter;
+
+import static androidx.heifwriter.HeifWriter.INPUT_MODE_BITMAP;
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.media.MediaMetadataRetriever;
+import android.opengl.GLES20;
+import android.os.Environment;
+import android.os.Handler;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+/**
+ * Base class holding common utilities for {@link HeifWriterTest} and {@link AvifWriterTest}.
+ */
+public class TestBase {
+ private static final String TAG = HeifWriterTest.class.getSimpleName();
+
+ private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+
+ private static final byte[][] TEST_YUV_COLORS = {
+ {(byte) 255, (byte) 0, (byte) 0},
+ {(byte) 255, (byte) 0, (byte) 255},
+ {(byte) 255, (byte) 255, (byte) 255},
+ {(byte) 255, (byte) 255, (byte) 0},
+ };
+ private static final byte[][] TEST_YUV_10BIT_COLORS = {
+ {(byte) 1023, (byte) 0, (byte) 0},
+ {(byte) 1023, (byte) 0, (byte) 1023},
+ {(byte) 1023, (byte) 1023, (byte) 1023},
+ {(byte) 1023, (byte) 1023, (byte) 0},
+ };
+ private static final Color COLOR_BLOCK =
+ Color.valueOf(1.0f, 1.0f, 1.0f);
+ private static final Color[] COLOR_BARS = {
+ Color.valueOf(0.0f, 0.0f, 0.0f),
+ Color.valueOf(0.0f, 0.0f, 0.64f),
+ Color.valueOf(0.0f, 0.64f, 0.0f),
+ Color.valueOf(0.0f, 0.64f, 0.64f),
+ Color.valueOf(0.64f, 0.0f, 0.0f),
+ Color.valueOf(0.64f, 0.0f, 0.64f),
+ Color.valueOf(0.64f, 0.64f, 0.0f),
+ };
+ private static final float MAX_DELTA = 0.025f;
+ private static final int BORDER_WIDTH = 16;
+
+ protected EglWindowSurface mInputEglSurface;
+ protected Handler mHandler;
+ protected int mInputIndex;
+ protected boolean mHighBitDepthEnabled = false;
+
+ protected long computePresentationTime(int frameIndex) {
+ return 132 + (long)frameIndex * 1000000;
+ }
+
+ protected void fillYuvBuffer(int frameIndex, @NonNull byte[] data, int width, int height,
+ @Nullable FileInputStream inputStream) throws IOException {
+ if (inputStream != null) {
+ inputStream.read(data);
+ } else {
+ byte[] color;
+ int sizeY = width * height;
+ if (!mHighBitDepthEnabled) {
+ color = TEST_YUV_COLORS[frameIndex % TEST_YUV_COLORS.length];
+ Arrays.fill(data, 0, sizeY, color[0]);
+ Arrays.fill(data, sizeY, sizeY * 5 / 4, color[1]);
+ Arrays.fill(data, sizeY * 5 / 4, sizeY * 3 / 2, color[2]);
+
+ } else {
+ color = TEST_YUV_10BIT_COLORS[frameIndex % TEST_YUV_10BIT_COLORS.length];
+ Arrays.fill(data, 0, sizeY, color[0]);
+ Arrays.fill(data, sizeY, sizeY * 2, color[1]);
+ Arrays.fill(data, sizeY * 2, sizeY * 3, color[2]);
+ }
+ }
+ }
+
+ protected static Rect getColorBarRect(int index, int width, int height) {
+ int barWidth = (width - BORDER_WIDTH * 2) / COLOR_BARS.length;
+ return new Rect(BORDER_WIDTH + barWidth * index, BORDER_WIDTH,
+ BORDER_WIDTH + barWidth * (index + 1), height - BORDER_WIDTH);
+ }
+
+ protected static Rect getColorBlockRect(int index, int width, int height) {
+ int blockCenterX = (width / 5) * (index % 4 + 1);
+ return new Rect(blockCenterX - width / 10, height / 6,
+ blockCenterX + width / 10, height / 3);
+ }
+
+ protected void generateSurfaceFrame(int frameIndex, int width, int height) {
+ GLES20.glViewport(0, 0, width, height);
+ GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
+ GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+ GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
+
+ for (int i = 0; i < COLOR_BARS.length; i++) {
+ Rect r = getColorBarRect(i, width, height);
+
+ GLES20.glScissor(r.left, r.top, r.width(), r.height());
+ final Color color = COLOR_BARS[i];
+ GLES20.glClearColor(color.red(), color.green(), color.blue(), 1.0f);
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+ }
+
+ Rect r = getColorBlockRect(frameIndex, width, height);
+ GLES20.glScissor(r.left, r.top, r.width(), r.height());
+ GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+ r.inset(BORDER_WIDTH, BORDER_WIDTH);
+ GLES20.glScissor(r.left, r.top, r.width(), r.height());
+ GLES20.glClearColor(COLOR_BLOCK.red(), COLOR_BLOCK.green(), COLOR_BLOCK.blue(), 1.0f);
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+ }
+
+ /**
+ * Determines if two color values are approximately equal.
+ */
+ protected static boolean approxEquals(Color expected, Color actual) {
+ return (Math.abs(expected.red() - actual.red()) <= MAX_DELTA)
+ && (Math.abs(expected.green() - actual.green()) <= MAX_DELTA)
+ && (Math.abs(expected.blue() - actual.blue()) <= MAX_DELTA);
+ }
+
+ protected void verifyResult(
+ String filename, int width, int height, int rotation,
+ int imageCount, int primary, boolean useGrid, boolean checkColor)
+ throws Exception {
+ MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+ retriever.setDataSource(filename);
+ String hasImage = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
+ if (!"yes".equals(hasImage)) {
+ throw new Exception("No images found in file " + filename);
+ }
+ assertEquals("Wrong width", width,
+ Integer.parseInt(retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH)));
+ assertEquals("Wrong height", height,
+ Integer.parseInt(retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)));
+ assertEquals("Wrong rotation", rotation,
+ Integer.parseInt(retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION)));
+ assertEquals("Wrong image count", imageCount,
+ Integer.parseInt(retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
+ assertEquals("Wrong primary index", primary,
+ Integer.parseInt(retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_IMAGE_PRIMARY)));
+ try {
+ retriever.release();
+ } catch (IOException e) {
+ // Nothing we can do about it.
+ }
+
+ if (useGrid) {
+ MediaExtractor extractor = new MediaExtractor();
+ extractor.setDataSource(filename);
+ MediaFormat format = extractor.getTrackFormat(0);
+ int tileWidth = format.getInteger(MediaFormat.KEY_TILE_WIDTH);
+ int tileHeight = format.getInteger(MediaFormat.KEY_TILE_HEIGHT);
+ int gridRows = format.getInteger(MediaFormat.KEY_GRID_ROWS);
+ int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLUMNS);
+ assertTrue("Wrong tile width or grid cols",
+ ((width + tileWidth - 1) / tileWidth) == gridCols);
+ assertTrue("Wrong tile height or grid rows",
+ ((height + tileHeight - 1) / tileHeight) == gridRows);
+ extractor.release();
+ }
+
+ if (checkColor) {
+ Bitmap bitmap = BitmapFactory.decodeFile(filename);
+
+ for (int i = 0; i < COLOR_BARS.length; i++) {
+ Rect r = getColorBarRect(i, width, height);
+ assertTrue("Color bar " + i + " doesn't match", approxEquals(COLOR_BARS[i],
+ Color.valueOf(bitmap.getPixel(r.centerX(), r.centerY()))));
+ }
+
+ Rect r = getColorBlockRect(primary, width, height);
+ assertTrue("Color block doesn't match", approxEquals(COLOR_BLOCK,
+ Color.valueOf(bitmap.getPixel(r.centerX(), height - r.centerY()))));
+
+ bitmap.recycle();
+ }
+ }
+
+ protected void closeQuietly(Closeable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (RuntimeException rethrown) {
+ throw rethrown;
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ protected int copy(InputStream in, OutputStream out) throws IOException {
+ int total = 0;
+ byte[] buffer = new byte[8192];
+ int c;
+ while ((c = in.read(buffer)) != -1) {
+ total += c;
+ out.write(buffer, 0, c);
+ }
+ return total;
+ }
+
+ protected boolean hasEncoderForMime(String mime) {
+ for (MediaCodecInfo info : sMCL.getCodecInfos()) {
+ if (info.isEncoder()) {
+ for (String type : info.getSupportedTypes()) {
+ if (type.equalsIgnoreCase(mime)) {
+ Log.i(TAG, "found codec " + info.getName() + " for mime " + mime);
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ protected void drawFrame(int width, int height) {
+ mInputEglSurface.makeCurrent();
+ generateSurfaceFrame(mInputIndex, width, height);
+ mInputEglSurface.setPresentationTime(1000 * computePresentationTime(mInputIndex));
+ mInputEglSurface.swapBuffers();
+ mInputIndex++;
+ }
+
+ protected static class TestConfig {
+ final int mInputMode;
+ final boolean mUseGrid;
+ final boolean mUseHandler;
+ final boolean mUseHighBitDepth;
+ final int mMaxNumImages;
+ final int mActualNumImages;
+ final int mWidth;
+ final int mHeight;
+ final int mRotation;
+ final int mQuality;
+ final String mInputPath;
+ final String mOutputPath;
+ final Bitmap[] mBitmaps;
+
+ TestConfig(int inputMode, boolean useGrid, boolean useHandler, boolean useHighBitDepth,
+ int maxNumImages, int actualNumImages, int width, int height, int rotation,
+ int quality, String inputPath, String outputPath, Bitmap[] bitmaps) {
+ mInputMode = inputMode;
+ mUseGrid = useGrid;
+ mUseHandler = useHandler;
+ mUseHighBitDepth = useHighBitDepth;
+ mMaxNumImages = maxNumImages;
+ mActualNumImages = actualNumImages;
+ mWidth = width;
+ mHeight = height;
+ mRotation = rotation;
+ mQuality = quality;
+ mInputPath = inputPath;
+ mOutputPath = outputPath;
+ mBitmaps = bitmaps;
+ }
+
+ static class Builder {
+ final int mInputMode;
+ final boolean mUseGrid;
+ final boolean mUseHandler;
+ boolean mUseHighBitDepth;
+ int mMaxNumImages;
+ int mNumImages;
+ int mWidth;
+ int mHeight;
+ int mRotation;
+ final int mQuality;
+ String mInputPath;
+ final String mOutputPath;
+ Bitmap[] mBitmaps;
+ boolean mNumImagesSetExplicitly;
+
+
+ Builder(int inputMode, boolean useGrids, boolean useHandler, String outputFileName) {
+ mInputMode = inputMode;
+ mUseGrid = useGrids;
+ mUseHandler = useHandler;
+ mUseHighBitDepth = false;
+ mMaxNumImages = mNumImages = 4;
+ mWidth = 1920;
+ mHeight = 1080;
+ mRotation = 0;
+ mQuality = 100;
+ mOutputPath = new File(getApplicationContext().getExternalFilesDir(null),
+ outputFileName).getAbsolutePath();
+ }
+
+ Builder setInputPath(String inputPath) {
+ mInputPath = (mInputMode == INPUT_MODE_BITMAP) ? inputPath : null;
+ return this;
+ }
+
+ Builder setNumImages(int numImages) {
+ mNumImagesSetExplicitly = true;
+ mNumImages = numImages;
+ return this;
+ }
+
+ Builder setRotation(int rotation) {
+ mRotation = rotation;
+ return this;
+ }
+
+ Builder setHighBitDepthEnabled(boolean useHighBitDepth) {
+ mUseHighBitDepth = useHighBitDepth;
+ return this;
+ }
+
+ private void loadBitmapInputs() {
+ if (mInputMode != INPUT_MODE_BITMAP) {
+ return;
+ }
+ if (!mUseHighBitDepth) {
+ MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+ retriever.setDataSource(mInputPath);
+ String hasImage = retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
+ if (!"yes".equals(hasImage)) {
+ throw new IllegalArgumentException("no bitmap found!");
+ }
+ mMaxNumImages = Math.min(mMaxNumImages,
+ Integer.parseInt(retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
+ if (!mNumImagesSetExplicitly) {
+ mNumImages = mMaxNumImages;
+ }
+ mBitmaps = new Bitmap[mMaxNumImages];
+ for (int i = 0; i < mBitmaps.length; i++) {
+ mBitmaps[i] = retriever.getImageAtIndex(i);
+ }
+ mWidth = mBitmaps[0].getWidth();
+ mHeight = mBitmaps[0].getHeight();
+ try {
+ retriever.release();
+ } catch (IOException e) {
+ // Nothing we can do about it.
+ }
+ } else {
+ mMaxNumImages = 1;
+ mNumImages = 1;
+ }
+ }
+
+ private void cleanupStaleOutputs() {
+ File outputFile = new File(mOutputPath);
+ if (outputFile.exists()) {
+ outputFile.delete();
+ }
+ }
+
+ TestConfig build() {
+ cleanupStaleOutputs();
+ loadBitmapInputs();
+
+ return new TestConfig(mInputMode, mUseGrid, mUseHandler, mUseHighBitDepth,
+ mMaxNumImages, mNumImages, mWidth, mHeight, mRotation, mQuality, mInputPath,
+ mOutputPath, mBitmaps);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "TestConfig"
+ + ": mInputMode " + mInputMode
+ + ", mUseGrid " + mUseGrid
+ + ", mUseHandler " + mUseHandler
+ + ", mMaxNumImages " + mMaxNumImages
+ + ", mNumImages " + mActualNumImages
+ + ", mWidth " + mWidth
+ + ", mHeight " + mHeight
+ + ", mRotation " + mRotation
+ + ", mQuality " + mQuality
+ + ", mInputPath " + mInputPath
+ + ", mOutputPath " + mOutputPath;
+ }
+ }
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/androidTest/res/raw/heifwriter_input10.png b/heifwriter/heifwriter/src/androidTest/res/raw/heifwriter_input10.png
new file mode 100644
index 0000000..55503df
--- /dev/null
+++ b/heifwriter/heifwriter/src/androidTest/res/raw/heifwriter_input10.png
Binary files differ
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifEncoder.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifEncoder.java
new file mode 100644
index 0000000..561e0b2
--- /dev/null
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifEncoder.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2022 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.heifwriter;
+
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecList;
+import android.media.MediaFormat;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.IOException;
+
+/**
+ * This class encodes images into HEIF-compatible samples using AV1 encoder.
+ *
+ * It currently supports three input modes: {@link #INPUT_MODE_BUFFER},
+ * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+ *
+ * The output format and samples are sent back in {@link
+ * Callback#onOutputFormatChanged(HeifEncoder, MediaFormat)} and {@link
+ * Callback#onDrainOutputBuffer(HeifEncoder, ByteBuffer)}. If the client
+ * requests to use grid, each tile will be sent back individually.
+ *
+ * HeifEncoder is made a separate class from {@link HeifWriter}, as some more
+ * advanced use cases might want to build solutions on top of the HeifEncoder directly.
+ * (eg. mux still images and video tracks into a single container).
+ *
+ * @hide
+ */
+public final class AvifEncoder extends EncoderBase {
+ private static final String TAG = "AvifEncoder";
+ private static final boolean DEBUG = false;
+
+ protected static final int GRID_WIDTH = 512;
+ protected static final int GRID_HEIGHT = 512;
+ protected static final double MAX_COMPRESS_RATIO = 0.25f;
+
+ private static final MediaCodecList sMCL =
+ new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+
+ /**
+ * Configure the avif encoding session. Should only be called once.
+ *
+ * @param width Width of the image.
+ * @param height Height of the image.
+ * @param useGrid Whether to encode image into tiles. If enabled, tile size will be
+ * automatically chosen.
+ * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best quality
+ * supported by this implementation (which often results in larger file size).
+ * @param inputMode The input type of this encoding session.
+ * @param handler If not null, client will receive all callbacks on the handler's looper.
+ * Otherwise, client will receive callbacks on a looper created by us.
+ * @param cb The callback to receive various messages from the avif encoder.
+ */
+ public AvifEncoder(int width, int height, boolean useGrid,
+ int quality, @InputMode int inputMode,
+ @Nullable Handler handler, @NonNull Callback cb,
+ boolean useBitDepth10) throws IOException {
+ super("AVIF", width, height, useGrid, quality, inputMode, handler, cb, useBitDepth10);
+ mEncoder.setCallback(new Av1EncoderCallback(), mHandler);
+ finishSettingUpEncoder(useBitDepth10);
+ }
+
+ protected static String findAv1Fallback() {
+ String av1 = null; // first AV1 encoder
+ for (MediaCodecInfo info : sMCL.getCodecInfos()) {
+ if (!info.isEncoder()) {
+ continue;
+ }
+ MediaCodecInfo.CodecCapabilities caps = null;
+ try {
+ caps = info.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AV1);
+ } catch (IllegalArgumentException e) { // mime is not supported
+ continue;
+ }
+ if (!caps.getVideoCapabilities().isSizeSupported(GRID_WIDTH, GRID_HEIGHT)) {
+ continue;
+ }
+ if (caps.getEncoderCapabilities().isBitrateModeSupported(
+ MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
+ // Encoder that supports CQ mode is preferred over others,
+ // return the first encoder that supports CQ mode.
+ // (No need to check if it's hw based, it's already listed in
+ // order of preference.)
+ return info.getName();
+ }
+ if (av1 == null) {
+ av1 = info.getName();
+ }
+ }
+ // If no encoders support CQ, return the first AV1 encoder.
+ return av1;
+ }
+
+ /**
+ * MediaCodec callback for AV1 encoding.
+ */
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ protected class Av1EncoderCallback extends EncoderCallback {
+ @Override
+ public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
+ if (codec != mEncoder) return;
+
+ if (DEBUG) Log.d(TAG, "onOutputFormatChanged: " + format);
+
+ // TODO(b/252835975) replace "image/avif" with MIMETYPE_IMAGE_AVIF.
+ if (!format.getString(MediaFormat.KEY_MIME).equals("image/avif")) {
+ format.setString(MediaFormat.KEY_MIME, "image/avif");
+ format.setInteger(MediaFormat.KEY_WIDTH, mWidth);
+ format.setInteger(MediaFormat.KEY_HEIGHT, mHeight);
+
+ if (mUseGrid) {
+ format.setInteger(MediaFormat.KEY_TILE_WIDTH, mGridWidth);
+ format.setInteger(MediaFormat.KEY_TILE_HEIGHT, mGridHeight);
+ format.setInteger(MediaFormat.KEY_GRID_ROWS, mGridRows);
+ format.setInteger(MediaFormat.KEY_GRID_COLUMNS, mGridCols);
+ }
+ }
+
+ mCallback.onOutputFormatChanged(AvifEncoder.this, format);
+ }
+ }
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifWriter.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifWriter.java
new file mode 100644
index 0000000..706f9dfd
--- /dev/null
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifWriter.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright 2022 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.heifwriter;
+
+import static android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_HEIF;
+
+import android.annotation.SuppressLint;
+import android.graphics.Bitmap;
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+import android.media.MediaMuxer;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Process;
+import android.util.Log;
+import android.util.Pair;
+import android.view.Surface;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * This class writes one or more still images (of the same dimensions) into
+ * an AVIF file.
+ *
+ * It currently supports three input modes: {@link #INPUT_MODE_BUFFER},
+ * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+ *
+ * The general sequence (in pseudo-code) to write a avif file using this class is as follows:
+ *
+ * 1) Construct the writer:
+ * AvifWriter avifwriter = new AvifWriter(...);
+ *
+ * 2) If using surface input mode, obtain the input surface:
+ * Surface surface = avifwriter.getInputSurface();
+ *
+ * 3) Call start:
+ * avifwriter.start();
+ *
+ * 4) Depending on the chosen input mode, add one or more images using one of these methods:
+ * avifwriter.addYuvBuffer(...); Or
+ * avifwriter.addBitmap(...); Or
+ * render to the previously obtained surface
+ *
+ * 5) Call stop:
+ * avifwriter.stop(...);
+ *
+ * 6) Close the writer:
+ * avifwriter.close();
+ *
+ * Please refer to the documentations on individual methods for the exact usage.
+ */
+@SuppressWarnings("HiddenSuperclass")
+public final class AvifWriter extends WriterBase {
+
+ private static final String TAG = "AvifWriter";
+ private static final boolean DEBUG = false;
+
+ /**
+ * The input mode where the client adds input buffers with YUV data.
+ *
+ * @see #addYuvBuffer(int, byte[])
+ */
+ public static final int INPUT_MODE_BUFFER = WriterBase.INPUT_MODE_BUFFER;
+
+ /**
+ * The input mode where the client renders the images to an input Surface created by the writer.
+ *
+ * The input surface operates in single buffer mode. As a result, for use case where camera
+ * directly outputs to the input surface, this mode will not work because camera framework
+ * requires multiple buffers to operate in a pipeline fashion.
+ *
+ * @see #getInputSurface()
+ */
+ public static final int INPUT_MODE_SURFACE = WriterBase.INPUT_MODE_SURFACE;
+
+ /**
+ * The input mode where the client adds bitmaps.
+ *
+ * @see #addBitmap(Bitmap)
+ */
+ public static final int INPUT_MODE_BITMAP = WriterBase.INPUT_MODE_BITMAP;
+
+ /**
+ * @hide
+ */
+ @IntDef({
+ INPUT_MODE_BUFFER, INPUT_MODE_SURFACE, INPUT_MODE_BITMAP,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface InputMode {
+
+ }
+
+ /**
+ * Builder class for constructing a AvifWriter object from specified parameters.
+ */
+ public static final class Builder {
+ private final String mPath;
+ private final FileDescriptor mFd;
+ private final int mWidth;
+ private final int mHeight;
+ private final @InputMode int mInputMode;
+ private boolean mGridEnabled = true;
+ private int mQuality = 100;
+ private int mMaxImages = 1;
+ private int mPrimaryIndex = 0;
+ private int mRotation = 0;
+ private Handler mHandler;
+ private boolean mHighBitDepthEnabled = false;
+
+ /**
+ * Construct a Builder with output specified by its path.
+ *
+ * @param path Path of the file to be written.
+ * @param width Width of the image in number of pixels.
+ * @param height Height of the image in number of pixels.
+ * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
+ * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+ */
+ public Builder(@NonNull String path,
+ @IntRange(from = 1) int width,
+ @IntRange(from = 1) int height,
+ @InputMode int inputMode) {
+ this(path, null, width, height, inputMode);
+ }
+
+ /**
+ * Construct a Builder with output specified by its file descriptor.
+ *
+ * @param fd File descriptor of the file to be written.
+ * @param width Width of the image in number of pixels.
+ * @param height Height of the image in number of pixels.
+ * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
+ * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+ */
+ public Builder(@NonNull FileDescriptor fd,
+ @IntRange(from = 1) int width,
+ @IntRange(from = 1) int height,
+ @InputMode int inputMode) {
+ this(null, fd, width, height, inputMode);
+ }
+
+ private Builder(String path, FileDescriptor fd,
+ @IntRange(from = 1) int width,
+ @IntRange(from = 1) int height,
+ @InputMode int inputMode) {
+ mPath = path;
+ mFd = fd;
+ mWidth = width;
+ mHeight = height;
+ mInputMode = inputMode;
+ }
+
+ /**
+ * Set the image rotation in degrees.
+ *
+ * @param rotation Rotation angle in degrees (clockwise) of the image, must be 0, 90,
+ * 180 or 270. Default is 0.
+ * @return this Builder object.
+ */
+ public @NonNull Builder setRotation(@IntRange(from = 0) int rotation) {
+ if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270) {
+ throw new IllegalArgumentException("Invalid rotation angle: " + rotation);
+ }
+ mRotation = rotation;
+ return this;
+ }
+
+ /**
+ * Set whether to enable grid option.
+ *
+ * @param gridEnabled Whether to enable grid option. If enabled, the tile size will be
+ * automatically chosen. Default is to enable.
+ * @return this Builder object.
+ */
+ public @NonNull Builder setGridEnabled(boolean gridEnabled) {
+ mGridEnabled = gridEnabled;
+ return this;
+ }
+
+ /**
+ * Set the quality for encoding images.
+ *
+ * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best
+ * quality supported by this implementation. Default is 100.
+ * @return this Builder object.
+ */
+ public @NonNull Builder setQuality(@IntRange(from = 0, to = 100) int quality) {
+ if (quality < 0 || quality > 100) {
+ throw new IllegalArgumentException("Invalid quality: " + quality);
+ }
+ mQuality = quality;
+ return this;
+ }
+
+ /**
+ * Set the maximum number of images to write.
+ *
+ * @param maxImages Max number of images to write. Frames exceeding this number will not be
+ * written to file. The writing can be stopped earlier before this number
+ * of images are written by {@link #stop(long)}, except for the input mode
+ * of {@link #INPUT_MODE_SURFACE}, where the EOS timestamp must be
+ * specified (via {@link #setInputEndOfStreamTimestamp(long)} and reached.
+ * Default is 1.
+ * @return this Builder object.
+ */
+ public @NonNull Builder setMaxImages(@IntRange(from = 1) int maxImages) {
+ if (maxImages <= 0) {
+ throw new IllegalArgumentException("Invalid maxImage: " + maxImages);
+ }
+ mMaxImages = maxImages;
+ return this;
+ }
+
+ /**
+ * Set the primary image index.
+ *
+ * @param primaryIndex Index of the image that should be marked as primary, must be within
+ * range [0, maxImages - 1] inclusive. Default is 0.
+ * @return this Builder object.
+ */
+ public @NonNull Builder setPrimaryIndex(@IntRange(from = 0) int primaryIndex) {
+ mPrimaryIndex = primaryIndex;
+ return this;
+ }
+
+ /**
+ * Provide a handler for the AvifWriter to use.
+ *
+ * @param handler If not null, client will receive all callbacks on the handler's looper.
+ * Otherwise, client will receive callbacks on a looper created by the
+ * writer. Default is null.
+ * @return this Builder object.
+ */
+ public @NonNull Builder setHandler(@Nullable Handler handler) {
+ mHandler = handler;
+ return this;
+ }
+
+ /**
+ * Provide a setting for the AvifWriter to use high bit-depth or not.
+ *
+ * @param highBitDepthEnabled Whether to enable high bit-depth mode. Default is false, if
+ * true, AvifWriter will encode with high bit-depth.
+ * @return this Builder object.
+ */
+ public @NonNull Builder setHighBitDepthEnabled(boolean highBitDepthEnabled) {
+ mHighBitDepthEnabled = highBitDepthEnabled;
+ return this;
+ }
+
+ /**
+ * Build a AvifWriter object.
+ *
+ * @return a AvifWriter object built according to the specifications.
+ * @throws IOException if failed to create the writer, possibly due to failure to create
+ * {@link android.media.MediaMuxer} or {@link android.media.MediaCodec}.
+ */
+ public @NonNull AvifWriter build() throws IOException {
+ return new AvifWriter(mPath, mFd, mWidth, mHeight, mRotation, mGridEnabled, mQuality,
+ mMaxImages, mPrimaryIndex, mInputMode, mHandler, mHighBitDepthEnabled);
+ }
+ }
+
+ @SuppressLint("WrongConstant")
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ AvifWriter(@NonNull String path,
+ @NonNull FileDescriptor fd,
+ int width,
+ int height,
+ int rotation,
+ boolean gridEnabled,
+ int quality,
+ int maxImages,
+ int primaryIndex,
+ @InputMode int inputMode,
+ @Nullable Handler handler,
+ boolean highBitDepthEnabled) throws IOException {
+ super(rotation, inputMode, maxImages, primaryIndex, gridEnabled, quality,
+ handler, highBitDepthEnabled);
+
+ if (DEBUG) {
+ Log.d(TAG, "width: " + width
+ + ", height: " + height
+ + ", rotation: " + rotation
+ + ", gridEnabled: " + gridEnabled
+ + ", quality: " + quality
+ + ", maxImages: " + maxImages
+ + ", primaryIndex: " + primaryIndex
+ + ", inputMode: " + inputMode);
+ }
+
+ // set to 1 initially, and wait for output format to know for sure
+ mNumTiles = 1;
+
+ mMuxer = (path != null) ? new MediaMuxer(path, MUXER_OUTPUT_HEIF)
+ : new MediaMuxer(fd, MUXER_OUTPUT_HEIF);
+
+ mEncoder = new AvifEncoder(width, height, gridEnabled, quality,
+ mInputMode, mHandler, new WriterCallback(), highBitDepthEnabled);
+ }
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java
index 35d34d4..c69e002 100644
--- a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java
@@ -25,6 +25,8 @@
import android.util.Log;
import android.view.Surface;
+import androidx.annotation.NonNull;
+
import java.util.Objects;
/**
@@ -52,18 +54,22 @@
* Creates an EglWindowSurface from a Surface.
*/
public EglWindowSurface(Surface surface) {
+ this(surface, false);
+ }
+
+ public EglWindowSurface(Surface surface, boolean useHighBitDepth) {
if (surface == null) {
throw new NullPointerException();
}
mSurface = surface;
- eglSetup();
+ eglSetup(useHighBitDepth);
}
/**
* Prepares EGL. We want a GLES 2.0 context and a surface that supports recording.
*/
- private void eglSetup() {
+ private void eglSetup(boolean useHighBitDepth) {
mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (Objects.equals(mEGLDisplay, EGL14.EGL_NO_DISPLAY)) {
throw new RuntimeException("unable to get EGL14 display");
@@ -76,27 +82,31 @@
// Configure EGL for recordable and OpenGL ES 2.0. We want enough RGB bits
// to minimize artifacts from possible YUV conversion.
- int[] attribList = {
- EGL14.EGL_RED_SIZE, 8,
- EGL14.EGL_GREEN_SIZE, 8,
- EGL14.EGL_BLUE_SIZE, 8,
- EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
- EGLExt.EGL_RECORDABLE_ANDROID, 1,
- EGL14.EGL_NONE
+ int eglColorSize = useHighBitDepth ? 10: 8;
+ int eglAlphaSize = useHighBitDepth ? 2: 0;
+ int recordable = useHighBitDepth ? 0: 1;
+ int[] configAttribList = {
+ EGL14.EGL_RED_SIZE, eglColorSize,
+ EGL14.EGL_GREEN_SIZE, eglColorSize,
+ EGL14.EGL_BLUE_SIZE, eglColorSize,
+ EGL14.EGL_ALPHA_SIZE, eglAlphaSize,
+ EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
+ EGLExt.EGL_RECORDABLE_ANDROID, recordable,
+ EGL14.EGL_NONE
};
int[] numConfigs = new int[1];
- if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, mConfigs, 0, mConfigs.length,
- numConfigs, 0)) {
+ if (!EGL14.eglChooseConfig(mEGLDisplay, configAttribList, 0, mConfigs, 0, mConfigs.length,
+ numConfigs, 0)) {
throw new RuntimeException("unable to find RGB888+recordable ES2 EGL config");
}
// Configure context for OpenGL ES 2.0.
- int[] attrib_list = {
- EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
- EGL14.EGL_NONE
+ int[] contextAttribList = {
+ EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
+ EGL14.EGL_NONE
};
mEGLContext = EGL14.eglCreateContext(mEGLDisplay, mConfigs[0], EGL14.EGL_NO_CONTEXT,
- attrib_list, 0);
+ contextAttribList, 0);
checkEglError("eglCreateContext");
if (mEGLContext == null) {
throw new RuntimeException("null context");
@@ -188,7 +198,7 @@
/**
* Returns the Surface that the MediaCodec receives buffers from.
*/
- public Surface getSurface() {
+ public @NonNull Surface getSurface() {
return mSurface;
}
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EncoderBase.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EncoderBase.java
new file mode 100644
index 0000000..13df5f2
--- /dev/null
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EncoderBase.java
@@ -0,0 +1,1067 @@
+/*
+ * Copyright 2022 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.heifwriter;
+
+import android.graphics.Bitmap;
+import android.graphics.ImageFormat;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.media.Image;
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodec.CodecException;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecList;
+import android.media.MediaFormat;
+import android.opengl.GLES20;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Process;
+import android.util.Log;
+import android.util.Range;
+import android.view.Surface;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * This class holds common utilities for {@link HeifEncoder} and {@link AvifEncoder}, and
+ * calls media framework and encodes images into HEIF- or AVIF- compatible samples using
+ * HEVC or AV1 encoder.
+ *
+ * It currently supports three input modes: {@link #INPUT_MODE_BUFFER},
+ * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+ *
+ * Callback#onOutputFormatChanged(MediaCodec, MediaFormat)} and {@link
+ * Callback#onDrainOutputBuffer(MediaCodec, ByteBuffer)}. If the client
+ * requests to use grid, each tile will be sent back individually.
+ *
+ *
+ * * HeifEncoder is made a separate class from {@link HeifWriter}, as some more
+ * * advanced use cases might want to build solutions on top of the HeifEncoder directly.
+ * * (eg. mux still images and video tracks into a single container).
+ *
+ *
+ * @hide
+ */
+public class EncoderBase implements AutoCloseable,
+ SurfaceTexture.OnFrameAvailableListener {
+ private static final String TAG = "EncoderBase";
+ private static final boolean DEBUG = false;
+
+ private String MIME;
+ private int GRID_WIDTH;
+ private int GRID_HEIGHT;
+ private double MAX_COMPRESS_RATIO;
+ private int INPUT_BUFFER_POOL_SIZE = 2;
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ MediaCodec mEncoder;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final MediaFormat mCodecFormat;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ protected final Callback mCallback;
+ private final HandlerThread mHandlerThread;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final Handler mHandler;
+ private final @InputMode int mInputMode;
+ private final boolean mUseBitDepth10;
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final int mWidth;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ protected final int mHeight;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ protected final int mGridWidth;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ protected final int mGridHeight;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ protected final int mGridRows;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ protected final int mGridCols;
+ private final int mNumTiles;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final boolean mUseGrid;
+
+ private int mInputIndex;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ boolean mInputEOS;
+ private final Rect mSrcRect;
+ private final Rect mDstRect;
+ private ByteBuffer mCurrentBuffer;
+ private final ArrayList<ByteBuffer> mEmptyBuffers = new ArrayList<>();
+ private final ArrayList<ByteBuffer> mFilledBuffers = new ArrayList<>();
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final ArrayList<Integer> mCodecInputBuffers = new ArrayList<>();
+ private final boolean mCopyTiles;
+
+ // Helper for tracking EOS when surface is used
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ SurfaceEOSTracker mEOSTracker;
+
+ // Below variables are to handle GL copy from client's surface
+ // to encoder surface when tiles are used.
+ private SurfaceTexture mInputTexture;
+ private Surface mInputSurface;
+ private Surface mEncoderSurface;
+ private EglWindowSurface mEncoderEglSurface;
+ private EglRectBlt mRectBlt;
+ private int mTextureId;
+ private final float[] mTmpMatrix = new float[16];
+ private final AtomicBoolean mStopping = new AtomicBoolean(false);
+
+ public static final int INPUT_MODE_BUFFER = HeifWriter.INPUT_MODE_BUFFER;
+ public static final int INPUT_MODE_SURFACE = HeifWriter.INPUT_MODE_SURFACE;
+ public static final int INPUT_MODE_BITMAP = HeifWriter.INPUT_MODE_BITMAP;
+ @IntDef({
+ INPUT_MODE_BUFFER,
+ INPUT_MODE_SURFACE,
+ INPUT_MODE_BITMAP,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface InputMode {}
+
+ public static abstract class Callback {
+ /**
+ * Called when the output format has changed.
+ *
+ * @param encoder The EncoderBase object.
+ * @param format The new output format.
+ */
+ public abstract void onOutputFormatChanged(
+ @NonNull EncoderBase encoder, @NonNull MediaFormat format);
+
+ /**
+ * Called when an output buffer becomes available.
+ *
+ * @param encoder The EncoderBase object.
+ * @param byteBuffer the available output buffer.
+ */
+ public abstract void onDrainOutputBuffer(
+ @NonNull EncoderBase encoder, @NonNull ByteBuffer byteBuffer);
+
+ /**
+ * Called when encoding reached the end of stream without error.
+ *
+ * @param encoder The EncoderBase object.
+ */
+ public abstract void onComplete(@NonNull EncoderBase encoder);
+
+ /**
+ * Called when encoding hits an error.
+ *
+ * @param encoder The EncoderBase object.
+ * @param e The exception that the codec reported.
+ */
+ public abstract void onError(@NonNull EncoderBase encoder, @NonNull CodecException e);
+ }
+
+ /**
+ * Configure the encoder. Should only be called once.
+ *
+ * @param mimeType mime type. Currently it supports "HEIC" and "AVIF".
+ * @param width Width of the image.
+ * @param height Height of the image.
+ * @param useGrid Whether to encode image into tiles. If enabled, tile size will be
+ * automatically chosen.
+ * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best quality
+ * supported by this implementation (which often results in larger file size).
+ * @param inputMode The input type of this encoding session.
+ * @param handler If not null, client will receive all callbacks on the handler's looper.
+ * Otherwise, client will receive callbacks on a looper created by us.
+ * @param cb The callback to receive various messages from the heif encoder.
+ */
+ protected EncoderBase(@NonNull String mimeType, int width, int height, boolean useGrid,
+ int quality, @InputMode int inputMode,
+ @Nullable Handler handler, @NonNull Callback cb,
+ boolean useBitDepth10) throws IOException {
+ if (DEBUG)
+ Log.d(TAG, "width: " + width + ", height: " + height +
+ ", useGrid: " + useGrid + ", quality: " + quality +
+ ", inputMode: " + inputMode +
+ ", useBitDepth10: " + String.valueOf(useBitDepth10));
+
+ if (width < 0 || height < 0 || quality < 0 || quality > 100) {
+ throw new IllegalArgumentException("invalid encoder inputs");
+ }
+
+ switch (mimeType) {
+ case "HEIC":
+ MIME = mimeType;
+ GRID_WIDTH = HeifEncoder.GRID_WIDTH;
+ GRID_HEIGHT = HeifEncoder.GRID_HEIGHT;
+ MAX_COMPRESS_RATIO = HeifEncoder.MAX_COMPRESS_RATIO;
+ break;
+ case "AVIF":
+ MIME = mimeType;
+ GRID_WIDTH = AvifEncoder.GRID_WIDTH;
+ GRID_HEIGHT = AvifEncoder.GRID_HEIGHT;
+ MAX_COMPRESS_RATIO = AvifEncoder.MAX_COMPRESS_RATIO;
+ break;
+ default:
+ Log.e(TAG, "Not supported mime type: " + mimeType);
+ }
+
+ boolean useHeicEncoder = false;
+ MediaCodecInfo.CodecCapabilities caps = null;
+ switch (MIME) {
+ case "HEIC":
+ try {
+ mEncoder = MediaCodec.createEncoderByType(
+ MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
+ caps = mEncoder.getCodecInfo().getCapabilitiesForType(
+ MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
+ // If the HEIC encoder can't support the size, fall back to HEVC encoder.
+ if (!caps.getVideoCapabilities().isSizeSupported(width, height)) {
+ mEncoder.release();
+ mEncoder = null;
+ throw new Exception();
+ }
+ useHeicEncoder = true;
+ } catch (Exception e) {
+ mEncoder = MediaCodec.createByCodecName(HeifEncoder.findHevcFallback());
+ caps = mEncoder.getCodecInfo()
+ .getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_HEVC);
+ // Disable grid if the image is too small
+ useGrid &= (width > GRID_WIDTH || height > GRID_HEIGHT);
+ // Always enable grid if the size is too large for the HEVC encoder
+ useGrid |= !caps.getVideoCapabilities().isSizeSupported(width, height);
+ }
+ break;
+ case "AVIF":
+ mEncoder = MediaCodec.createByCodecName(AvifEncoder.findAv1Fallback());
+ caps = mEncoder.getCodecInfo()
+ .getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AV1);
+ // Disable grid if the image is too small
+ useGrid &= (width > GRID_WIDTH || height > GRID_HEIGHT);
+ // Always enable grid if the size is too large for the AV1 encoder
+ useGrid |= !caps.getVideoCapabilities().isSizeSupported(width, height);
+ break;
+ default:
+ Log.e(TAG, "Not supported mime type: " + MIME);
+ }
+
+ mInputMode = inputMode;
+ mUseBitDepth10 = useBitDepth10;
+ mCallback = cb;
+
+ Looper looper = (handler != null) ? handler.getLooper() : null;
+ if (looper == null) {
+ mHandlerThread = new HandlerThread("HeifEncoderThread",
+ Process.THREAD_PRIORITY_FOREGROUND);
+ mHandlerThread.start();
+ looper = mHandlerThread.getLooper();
+ } else {
+ mHandlerThread = null;
+ }
+ mHandler = new Handler(looper);
+ boolean useSurfaceInternally =
+ (inputMode == INPUT_MODE_SURFACE) || (inputMode == INPUT_MODE_BITMAP);
+ int colorFormat = useSurfaceInternally ? CodecCapabilities.COLOR_FormatSurface :
+ (useBitDepth10 ? CodecCapabilities.COLOR_FormatYUVP010 :
+ CodecCapabilities.COLOR_FormatYUV420Flexible);
+ mCopyTiles = (useGrid && !useHeicEncoder) || (inputMode == INPUT_MODE_BITMAP);
+
+ mWidth = width;
+ mHeight = height;
+ mUseGrid = useGrid;
+
+ int gridWidth, gridHeight, gridRows, gridCols;
+
+ if (useGrid) {
+ gridWidth = GRID_WIDTH;
+ gridHeight = GRID_HEIGHT;
+ gridRows = (height + GRID_HEIGHT - 1) / GRID_HEIGHT;
+ gridCols = (width + GRID_WIDTH - 1) / GRID_WIDTH;
+ } else {
+ gridWidth = mWidth;
+ gridHeight = mHeight;
+ gridRows = 1;
+ gridCols = 1;
+ }
+
+ MediaFormat codecFormat;
+ if (useHeicEncoder) {
+ codecFormat = MediaFormat.createVideoFormat(
+ MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC, mWidth, mHeight);
+ } else {
+ codecFormat = MediaFormat.createVideoFormat(
+ MediaFormat.MIMETYPE_VIDEO_HEVC, gridWidth, gridHeight);
+ }
+
+ if (useGrid) {
+ codecFormat.setInteger(MediaFormat.KEY_TILE_WIDTH, gridWidth);
+ codecFormat.setInteger(MediaFormat.KEY_TILE_HEIGHT, gridHeight);
+ codecFormat.setInteger(MediaFormat.KEY_GRID_COLUMNS, gridCols);
+ codecFormat.setInteger(MediaFormat.KEY_GRID_ROWS, gridRows);
+ }
+
+ if (useHeicEncoder) {
+ mGridWidth = width;
+ mGridHeight = height;
+ mGridRows = 1;
+ mGridCols = 1;
+ } else {
+ mGridWidth = gridWidth;
+ mGridHeight = gridHeight;
+ mGridRows = gridRows;
+ mGridCols = gridCols;
+ }
+ mNumTiles = mGridRows * mGridCols;
+
+ codecFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 0);
+ codecFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
+ codecFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mNumTiles);
+
+ // When we're doing tiles, set the operating rate higher as the size
+ // is small, otherwise set to the normal 30fps.
+ if (mNumTiles > 1) {
+ codecFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, 120);
+ } else {
+ codecFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, 30);
+ }
+
+ if (useSurfaceInternally && !mCopyTiles) {
+ // Use fixed PTS gap and disable backward frame drop
+ Log.d(TAG, "Setting fixed pts gap");
+ codecFormat.setLong(MediaFormat.KEY_MAX_PTS_GAP_TO_ENCODER, -1000000);
+ }
+
+ MediaCodecInfo.EncoderCapabilities encoderCaps = caps.getEncoderCapabilities();
+
+ if (encoderCaps.isBitrateModeSupported(
+ MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
+ Log.d(TAG, "Setting bitrate mode to constant quality");
+ Range<Integer> qualityRange = encoderCaps.getQualityRange();
+ Log.d(TAG, "Quality range: " + qualityRange);
+ codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
+ MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ);
+ codecFormat.setInteger(MediaFormat.KEY_QUALITY, (int) (qualityRange.getLower() +
+ (qualityRange.getUpper() - qualityRange.getLower()) * quality / 100.0));
+ } else {
+ if (encoderCaps.isBitrateModeSupported(
+ MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR)) {
+ Log.d(TAG, "Setting bitrate mode to constant bitrate");
+ codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
+ MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
+ } else { // assume VBR
+ Log.d(TAG, "Setting bitrate mode to variable bitrate");
+ codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
+ MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
+ }
+ // Calculate the bitrate based on image dimension, max compression ratio and quality.
+ // Note that we set the frame rate to the number of tiles, so the bitrate would be the
+ // intended bits for one image.
+ int bitrate = caps.getVideoCapabilities().getBitrateRange().clamp(
+ (int) (width * height * 1.5 * 8 * MAX_COMPRESS_RATIO * quality / 100.0f));
+ codecFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
+ }
+
+ mCodecFormat = codecFormat;
+
+ mDstRect = new Rect(0, 0, mGridWidth, mGridHeight);
+ mSrcRect = new Rect();
+ }
+
+ /**
+ * Finish setting up the encoder.
+ * Call MediaCodec.configure() method so that mEncoder enters configured stage, then add input
+ * surface or add input buffers if needed.
+ *
+ * Note: this method must be called after the constructor.
+ */
+ protected void finishSettingUpEncoder(boolean useBitDepth10) {
+ boolean useSurfaceInternally =
+ (mInputMode == INPUT_MODE_SURFACE) || (mInputMode == INPUT_MODE_BITMAP);
+
+ mEncoder.configure(mCodecFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+
+ if (useSurfaceInternally) {
+ mEncoderSurface = mEncoder.createInputSurface();
+
+ mEOSTracker = new SurfaceEOSTracker(mCopyTiles);
+
+ if (mCopyTiles) {
+ mEncoderEglSurface = new EglWindowSurface(mEncoderSurface, useBitDepth10);
+ mEncoderEglSurface.makeCurrent();
+
+ mRectBlt = new EglRectBlt(
+ new Texture2dProgram((mInputMode == INPUT_MODE_BITMAP)
+ ? Texture2dProgram.TEXTURE_2D
+ : Texture2dProgram.TEXTURE_EXT),
+ mWidth, mHeight);
+
+ mTextureId = mRectBlt.createTextureObject();
+
+ if (mInputMode == INPUT_MODE_SURFACE) {
+ // use single buffer mode to block on input
+ mInputTexture = new SurfaceTexture(mTextureId, true);
+ mInputTexture.setOnFrameAvailableListener(this);
+ mInputTexture.setDefaultBufferSize(mWidth, mHeight);
+ mInputSurface = new Surface(mInputTexture);
+ }
+
+ // make uncurrent since onFrameAvailable could be called on arbituray thread.
+ // making the context current on a different thread will cause error.
+ mEncoderEglSurface.makeUnCurrent();
+ } else {
+ mInputSurface = mEncoderSurface;
+ }
+ } else {
+ for (int i = 0; i < INPUT_BUFFER_POOL_SIZE; i++) {
+ int bufferSize = mUseBitDepth10 ? mWidth * mHeight * 3 : mWidth * mHeight * 3 / 2;
+ mEmptyBuffers.add(ByteBuffer.allocateDirect(bufferSize));
+ }
+ }
+ }
+
+ /**
+ * Copies from source frame to encoder inputs using GL. The source could be either
+ * client's input surface, or the input bitmap loaded to texture.
+ */
+ private void copyTilesGL() {
+ GLES20.glViewport(0, 0, mGridWidth, mGridHeight);
+
+ for (int row = 0; row < mGridRows; row++) {
+ for (int col = 0; col < mGridCols; col++) {
+ int left = col * mGridWidth;
+ int top = row * mGridHeight;
+ mSrcRect.set(left, top, left + mGridWidth, top + mGridHeight);
+ try {
+ mRectBlt.copyRect(mTextureId, Texture2dProgram.V_FLIP_MATRIX, mSrcRect);
+ } catch (RuntimeException e) {
+ // EGL copy could throw if the encoder input surface is no longer valid
+ // after encoder is released. This is not an error because we're already
+ // stopping (either after EOS is received or requested by client).
+ if (mStopping.get()) {
+ return;
+ }
+ throw e;
+ }
+ mEncoderEglSurface.setPresentationTime(
+ 1000 * computePresentationTime(mInputIndex++));
+ mEncoderEglSurface.swapBuffers();
+ }
+ }
+ }
+
+ @Override
+ public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+ synchronized (this) {
+ if (mEncoderEglSurface == null) {
+ return;
+ }
+
+ mEncoderEglSurface.makeCurrent();
+
+ surfaceTexture.updateTexImage();
+ surfaceTexture.getTransformMatrix(mTmpMatrix);
+
+ long timestampNs = surfaceTexture.getTimestamp();
+
+ if (DEBUG) Log.d(TAG, "onFrameAvailable: timestampUs " + (timestampNs / 1000));
+
+ boolean takeFrame = mEOSTracker.updateLastInputAndEncoderTime(timestampNs,
+ computePresentationTime(mInputIndex + mNumTiles - 1));
+
+ if (takeFrame) {
+ copyTilesGL();
+ }
+
+ surfaceTexture.releaseTexImage();
+
+ // make uncurrent since the onFrameAvailable could be called on arbituray thread.
+ // making the context current on a different thread will cause error.
+ mEncoderEglSurface.makeUnCurrent();
+ }
+ }
+
+ /**
+ * Start the encoding process.
+ */
+ public void start() {
+ mEncoder.start();
+ }
+
+ /**
+ * Add one YUV buffer to be encoded. This might block if the encoder can't process the input
+ * buffers fast enough.
+ *
+ * After the call returns, the client can reuse the data array.
+ *
+ * @param format The YUV format as defined in {@link android.graphics.ImageFormat}, currently
+ * only support YUV_420_888.
+ *
+ * @param data byte array containing the YUV data. If the format has more than one planes,
+ * they must be concatenated.
+ */
+ public void addYuvBuffer(int format, @NonNull byte[] data) {
+ if (mInputMode != INPUT_MODE_BUFFER) {
+ throw new IllegalStateException(
+ "addYuvBuffer is only allowed in buffer input mode");
+ }
+ if ((mUseBitDepth10 && format != ImageFormat.YCBCR_P010)
+ || (!mUseBitDepth10 && format != ImageFormat.YUV_420_888)) {
+ throw new IllegalStateException("Wrong color format.");
+ }
+ if (data == null
+ || (mUseBitDepth10 && data.length != mWidth * mHeight * 3)
+ || (!mUseBitDepth10 && data.length != mWidth * mHeight * 3 / 2)) {
+ throw new IllegalArgumentException("invalid data");
+ }
+ addYuvBufferInternal(data);
+ }
+
+ /**
+ * Retrieves the input surface for encoding.
+ *
+ * Will only return valid value if configured to use surface input.
+ */
+ public @NonNull Surface getInputSurface() {
+ if (mInputMode != INPUT_MODE_SURFACE) {
+ throw new IllegalStateException(
+ "getInputSurface is only allowed in surface input mode");
+ }
+ return mInputSurface;
+ }
+
+ /**
+ * Sets the timestamp (in nano seconds) of the last input frame to encode. Frames with
+ * timestamps larger than the specified value will not be encoded. However, if a frame
+ * already started encoding when this is set, all tiles within that frame will be encoded.
+ *
+ * This method only applies when surface is used.
+ */
+ public void setEndOfInputStreamTimestamp(long timestampNs) {
+ if (mInputMode != INPUT_MODE_SURFACE) {
+ throw new IllegalStateException(
+ "setEndOfInputStreamTimestamp is only allowed in surface input mode");
+ }
+ if (mEOSTracker != null) {
+ mEOSTracker.updateInputEOSTime(timestampNs);
+ }
+ }
+
+ /**
+ * Adds one bitmap to be encoded.
+ */
+ public void addBitmap(@NonNull Bitmap bitmap) {
+ if (mInputMode != INPUT_MODE_BITMAP) {
+ throw new IllegalStateException("addBitmap is only allowed in bitmap input mode");
+ }
+
+ boolean takeFrame = mEOSTracker.updateLastInputAndEncoderTime(
+ computePresentationTime(mInputIndex) * 1000,
+ computePresentationTime(mInputIndex + mNumTiles - 1));
+
+ if (!takeFrame) return;
+
+ synchronized (this) {
+ if (mEncoderEglSurface == null) {
+ return;
+ }
+
+ mEncoderEglSurface.makeCurrent();
+
+ mRectBlt.loadTexture(mTextureId, bitmap);
+
+ copyTilesGL();
+
+ // make uncurrent since the onFrameAvailable could be called on arbituray thread.
+ // making the context current on a different thread will cause error.
+ mEncoderEglSurface.makeUnCurrent();
+ }
+ }
+
+ /**
+ * Sends input EOS to the encoder. Result will be notified asynchronously via
+ * {@link Callback#onComplete(EncoderBase)} if encoder reaches EOS without error, or
+ * {@link Callback#onError(EncoderBase, CodecException)} otherwise.
+ */
+ public void stopAsync() {
+ if (mInputMode == INPUT_MODE_BITMAP) {
+ // here we simply set the EOS timestamp to 0, so that the cut off will be the last
+ // bitmap ever added.
+ mEOSTracker.updateInputEOSTime(0);
+ } else if (mInputMode == INPUT_MODE_BUFFER) {
+ addYuvBufferInternal(null);
+ }
+ }
+
+ /**
+ * Generates the presentation time for input frame N, in microseconds.
+ * The timestamp advances 1 sec for every whole frame.
+ */
+ private long computePresentationTime(int frameIndex) {
+ return 132 + (long)frameIndex * 1000000 / mNumTiles;
+ }
+
+ /**
+ * Obtains one empty input buffer and copies the data into it. Before input
+ * EOS is sent, this would block until the data is copied. After input EOS
+ * is sent, this would return immediately.
+ */
+ private void addYuvBufferInternal(@Nullable byte[] data) {
+ ByteBuffer buffer = acquireEmptyBuffer();
+ if (buffer == null) {
+ return;
+ }
+ buffer.clear();
+ if (data != null) {
+ buffer.put(data);
+ }
+ buffer.flip();
+ synchronized (mFilledBuffers) {
+ mFilledBuffers.add(buffer);
+ }
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ maybeCopyOneTileYUV();
+ }
+ });
+ }
+
+ /**
+ * Routine to copy one tile if we have both input and codec buffer available.
+ *
+ * Must be called on the handler looper that also handles the MediaCodec callback.
+ */
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ void maybeCopyOneTileYUV() {
+ ByteBuffer currentBuffer;
+ while ((currentBuffer = getCurrentBuffer()) != null && !mCodecInputBuffers.isEmpty()) {
+ int index = mCodecInputBuffers.remove(0);
+
+ // 0-length input means EOS.
+ boolean inputEOS = (mInputIndex % mNumTiles == 0) && (currentBuffer.remaining() == 0);
+
+ if (!inputEOS) {
+ Image image = mEncoder.getInputImage(index);
+ int left = mGridWidth * (mInputIndex % mGridCols);
+ int top = mGridHeight * (mInputIndex / mGridCols % mGridRows);
+ mSrcRect.set(left, top, left + mGridWidth, top + mGridHeight);
+ copyOneTileYUV(currentBuffer, image, mWidth, mHeight, mSrcRect, mDstRect,
+ mUseBitDepth10);
+ }
+
+ mEncoder.queueInputBuffer(index, 0,
+ inputEOS ? 0 : mEncoder.getInputBuffer(index).capacity(),
+ computePresentationTime(mInputIndex++),
+ inputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
+
+ if (inputEOS || mInputIndex % mNumTiles == 0) {
+ returnEmptyBufferAndNotify(inputEOS);
+ }
+ }
+ }
+
+ /**
+ * Copies from a rect from src buffer to dst image.
+ * TOOD: This will be replaced by JNI.
+ */
+ private static void copyOneTileYUV(ByteBuffer srcBuffer, Image dstImage,
+ int srcWidth, int srcHeight, Rect srcRect, Rect dstRect, boolean useBitDepth10) {
+ if (srcRect.width() != dstRect.width() || srcRect.height() != dstRect.height()) {
+ throw new IllegalArgumentException("src and dst rect size are different!");
+ }
+ if (srcWidth % 2 != 0 || srcHeight % 2 != 0 ||
+ srcRect.left % 2 != 0 || srcRect.top % 2 != 0 ||
+ srcRect.right % 2 != 0 || srcRect.bottom % 2 != 0 ||
+ dstRect.left % 2 != 0 || dstRect.top % 2 != 0 ||
+ dstRect.right % 2 != 0 || dstRect.bottom % 2 != 0) {
+ throw new IllegalArgumentException("src or dst are not aligned!");
+ }
+
+ Image.Plane[] planes = dstImage.getPlanes();
+ if (useBitDepth10) {
+ // Assume pixel format is P010
+ // Y plane, UV interlaced
+ // pixel step = 2
+ for (int n = 0; n < planes.length; n++) {
+ ByteBuffer dstBuffer = planes[n].getBuffer();
+ int colStride = planes[n].getPixelStride();
+ int copyWidth = Math.min(srcRect.width(), srcWidth - srcRect.left);
+ int copyHeight = Math.min(srcRect.height(), srcHeight - srcRect.top);
+ int srcPlanePos = 0, div = 1;
+ if (n > 0) {
+ div = 2;
+ srcPlanePos = srcWidth * srcHeight;
+ if (n == 2) {
+ srcPlanePos += colStride / 2;
+ }
+ }
+ for (int i = 0; i < copyHeight / div; i++) {
+ srcBuffer.position(srcPlanePos +
+ (i + srcRect.top / div) * srcWidth + srcRect.left / div);
+ dstBuffer.position((i + dstRect.top / div) * planes[n].getRowStride()
+ + dstRect.left * colStride / div);
+
+ for (int j = 0; j < copyWidth / div; j++) {
+ dstBuffer.put(srcBuffer.get());
+ dstBuffer.put(srcBuffer.get());
+ if (colStride > 2 /*pixel step*/ && j != copyWidth / div - 1) {
+ dstBuffer.position(dstBuffer.position() + colStride / 2);
+ }
+ }
+ }
+ }
+ } else {
+ // Assume pixel format is YUV_420_Planer
+ // pixel step = 1
+ for (int n = 0; n < planes.length; n++) {
+ ByteBuffer dstBuffer = planes[n].getBuffer();
+ int colStride = planes[n].getPixelStride();
+ int copyWidth = Math.min(srcRect.width(), srcWidth - srcRect.left);
+ int copyHeight = Math.min(srcRect.height(), srcHeight - srcRect.top);
+ int srcPlanePos = 0, div = 1;
+ if (n > 0) {
+ div = 2;
+ srcPlanePos = srcWidth * srcHeight * (n + 3) / 4;
+ }
+ for (int i = 0; i < copyHeight / div; i++) {
+ srcBuffer.position(srcPlanePos +
+ (i + srcRect.top / div) * srcWidth / div + srcRect.left / div);
+ dstBuffer.position((i + dstRect.top / div) * planes[n].getRowStride()
+ + dstRect.left * colStride / div);
+
+ for (int j = 0; j < copyWidth / div; j++) {
+ dstBuffer.put(srcBuffer.get());
+ if (colStride > 1 && j != copyWidth / div - 1) {
+ dstBuffer.position(dstBuffer.position() + colStride - 1);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private ByteBuffer acquireEmptyBuffer() {
+ synchronized (mEmptyBuffers) {
+ // wait for an empty input buffer first
+ while (!mInputEOS && mEmptyBuffers.isEmpty()) {
+ try {
+ mEmptyBuffers.wait();
+ } catch (InterruptedException e) {}
+ }
+
+ // if already EOS, return null to stop further encoding.
+ return mInputEOS ? null : mEmptyBuffers.remove(0);
+ }
+ }
+
+ /**
+ * Routine to get the current input buffer to copy from.
+ * Only called on callback handler thread.
+ */
+ private ByteBuffer getCurrentBuffer() {
+ if (!mInputEOS && mCurrentBuffer == null) {
+ synchronized (mFilledBuffers) {
+ mCurrentBuffer = mFilledBuffers.isEmpty() ?
+ null : mFilledBuffers.remove(0);
+ }
+ }
+ return mInputEOS ? null : mCurrentBuffer;
+ }
+
+ /**
+ * Routine to put the consumed input buffer back into the empty buffer pool.
+ * Only called on callback handler thread.
+ */
+ private void returnEmptyBufferAndNotify(boolean inputEOS) {
+ synchronized (mEmptyBuffers) {
+ mInputEOS |= inputEOS;
+ mEmptyBuffers.add(mCurrentBuffer);
+ mEmptyBuffers.notifyAll();
+ }
+ mCurrentBuffer = null;
+ }
+
+ /**
+ * Routine to release all resources. Must be run on the same looper that
+ * handles the MediaCodec callbacks.
+ */
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ void stopInternal() {
+ if (DEBUG) Log.d(TAG, "stopInternal");
+
+ // set stopping, so that the tile copy would bail out
+ // if it hits failure after this point.
+ mStopping.set(true);
+
+ // after start, mEncoder is only accessed on handler, so no need to sync.
+ try {
+ if (mEncoder != null) {
+ mEncoder.stop();
+ mEncoder.release();
+ }
+ } catch (Exception e) {
+ } finally {
+ mEncoder = null;
+ }
+
+ // unblock the addBuffer() if we're tearing down before EOS is sent.
+ synchronized (mEmptyBuffers) {
+ mInputEOS = true;
+ mEmptyBuffers.notifyAll();
+ }
+
+ // Clean up surface and Egl related refs. This lock must come after encoder
+ // release. When we're closing, we insert stopInternal() at the front of queue
+ // so that the shutdown can be processed promptly, this means there might be
+ // some output available requests queued after this. As the tile copies trying
+ // to finish the current frame, there is a chance is might get stuck because
+ // those outputs were not returned. Shutting down the encoder will make break
+ // the tile copier out of that.
+ synchronized(this) {
+ try {
+ if (mRectBlt != null) {
+ mRectBlt.release(false);
+ }
+ } catch (Exception e) {
+ } finally {
+ mRectBlt = null;
+ }
+
+ try {
+ if (mEncoderEglSurface != null) {
+ // Note that this frees mEncoderSurface too. If mEncoderEglSurface is not
+ // there, client is responsible to release the input surface it got from us,
+ // we don't release mEncoderSurface here.
+ mEncoderEglSurface.release();
+ }
+ } catch (Exception e) {
+ } finally {
+ mEncoderEglSurface = null;
+ }
+
+ try {
+ if (mInputTexture != null) {
+ mInputTexture.release();
+ }
+ } catch (Exception e) {
+ } finally {
+ mInputTexture = null;
+ }
+ }
+ }
+
+ /**
+ * This class handles EOS for surface or bitmap inputs.
+ *
+ * When encoding from surface or bitmap, we can't call
+ * {@link MediaCodec#signalEndOfInputStream()} immediately after input is drawn, since this
+ * could drop all pending frames in the buffer queue. When there are tiles, this could leave
+ * us a partially encoded image.
+ *
+ * So here we track the EOS status by timestamps, and only signal EOS to the encoder
+ * when we collected all images we need.
+ *
+ * Since this is updated from multiple threads ({@link #setEndOfInputStreamTimestamp(long)},
+ * {@link EncoderCallback#onOutputBufferAvailable(MediaCodec, int, BufferInfo)},
+ * {@link #addBitmap(Bitmap)} and {@link #onFrameAvailable(SurfaceTexture)}), it must be fully
+ * synchronized.
+ *
+ * Note that when buffer input is used, the EOS flag is set in
+ * {@link EncoderCallback#onInputBufferAvailable(MediaCodec, int)} and this class is not used.
+ */
+ private class SurfaceEOSTracker {
+ private static final boolean DEBUG_EOS = false;
+
+ final boolean mCopyTiles;
+ long mInputEOSTimeNs = -1;
+ long mLastInputTimeNs = -1;
+ long mEncoderEOSTimeUs = -1;
+ long mLastEncoderTimeUs = -1;
+ long mLastOutputTimeUs = -1;
+ boolean mSignaled;
+
+ SurfaceEOSTracker(boolean copyTiles) {
+ mCopyTiles = copyTiles;
+ }
+
+ synchronized void updateInputEOSTime(long timestampNs) {
+ if (DEBUG_EOS) Log.d(TAG, "updateInputEOSTime: " + timestampNs);
+
+ if (mCopyTiles) {
+ if (mInputEOSTimeNs < 0) {
+ mInputEOSTimeNs = timestampNs;
+ }
+ } else {
+ if (mEncoderEOSTimeUs < 0) {
+ mEncoderEOSTimeUs = timestampNs / 1000;
+ }
+ }
+ updateEOSLocked();
+ }
+
+ synchronized boolean updateLastInputAndEncoderTime(long inputTimeNs, long encoderTimeUs) {
+ if (DEBUG_EOS) Log.d(TAG,
+ "updateLastInputAndEncoderTime: " + inputTimeNs + ", " + encoderTimeUs);
+
+ boolean shouldTakeFrame = mInputEOSTimeNs < 0 || inputTimeNs <= mInputEOSTimeNs;
+ if (shouldTakeFrame) {
+ mLastEncoderTimeUs = encoderTimeUs;
+ }
+ mLastInputTimeNs = inputTimeNs;
+ updateEOSLocked();
+ return shouldTakeFrame;
+ }
+
+ synchronized void updateLastOutputTime(long outputTimeUs) {
+ if (DEBUG_EOS) Log.d(TAG, "updateLastOutputTime: " + outputTimeUs);
+
+ mLastOutputTimeUs = outputTimeUs;
+ updateEOSLocked();
+ }
+
+ private void updateEOSLocked() {
+ if (mSignaled) {
+ return;
+ }
+ if (mEncoderEOSTimeUs < 0) {
+ if (mInputEOSTimeNs >= 0 && mLastInputTimeNs >= mInputEOSTimeNs) {
+ if (mLastEncoderTimeUs < 0) {
+ doSignalEOSLocked();
+ return;
+ }
+ // mEncoderEOSTimeUs tracks the timestamp of the last output buffer we
+ // will wait for. When that buffer arrives, encoder will be signalled EOS.
+ mEncoderEOSTimeUs = mLastEncoderTimeUs;
+ if (DEBUG_EOS) Log.d(TAG,
+ "updateEOSLocked: mEncoderEOSTimeUs " + mEncoderEOSTimeUs);
+ }
+ }
+ if (mEncoderEOSTimeUs >= 0 && mEncoderEOSTimeUs <= mLastOutputTimeUs) {
+ doSignalEOSLocked();
+ }
+ }
+
+ private void doSignalEOSLocked() {
+ if (DEBUG_EOS) Log.d(TAG, "doSignalEOSLocked");
+
+ mHandler.post(new Runnable() {
+ @Override public void run() {
+ if (mEncoder != null) {
+ mEncoder.signalEndOfInputStream();
+ }
+ }
+ });
+
+ mSignaled = true;
+ }
+ }
+
+
+ /**
+ * MediaCodec callback for HEVC/AV1 encoding.
+ */
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ abstract class EncoderCallback extends MediaCodec.Callback {
+ private boolean mOutputEOS;
+
+ @Override
+ public void onInputBufferAvailable(MediaCodec codec, int index) {
+ if (codec != mEncoder || mInputEOS) return;
+
+ if (DEBUG) Log.d(TAG, "onInputBufferAvailable: " + index);
+ mCodecInputBuffers.add(index);
+ maybeCopyOneTileYUV();
+ }
+
+ @Override
+ public void onOutputBufferAvailable(MediaCodec codec, int index, BufferInfo info) {
+ if (codec != mEncoder || mOutputEOS) return;
+
+ if (DEBUG) {
+ Log.d(TAG, "onOutputBufferAvailable: " + index
+ + ", time " + info.presentationTimeUs
+ + ", size " + info.size
+ + ", flags " + info.flags);
+ }
+
+ if ((info.size > 0) && ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0)) {
+ ByteBuffer outputBuffer = codec.getOutputBuffer(index);
+
+ // reset position as addBuffer() modifies it
+ outputBuffer.position(info.offset);
+ outputBuffer.limit(info.offset + info.size);
+
+ if (mEOSTracker != null) {
+ mEOSTracker.updateLastOutputTime(info.presentationTimeUs);
+ }
+
+ mCallback.onDrainOutputBuffer(EncoderBase.this, outputBuffer);
+ }
+
+ mOutputEOS |= ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0);
+
+ codec.releaseOutputBuffer(index, false);
+
+ if (mOutputEOS) {
+ stopAndNotify(null);
+ }
+ }
+
+ @Override
+ public void onError(MediaCodec codec, CodecException e) {
+ if (codec != mEncoder) return;
+
+ Log.e(TAG, "onError: " + e);
+ stopAndNotify(e);
+ }
+
+ private void stopAndNotify(@Nullable CodecException e) {
+ stopInternal();
+ if (e == null) {
+ mCallback.onComplete(EncoderBase.this);
+ } else {
+ mCallback.onError(EncoderBase.this, e);
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ // unblock the addBuffer() if we're tearing down before EOS is sent.
+ synchronized (mEmptyBuffers) {
+ mInputEOS = true;
+ mEmptyBuffers.notifyAll();
+ }
+
+ mHandler.postAtFrontOfQueue(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ stopInternal();
+ } catch (Exception e) {
+ // We don't want to crash when closing.
+ }
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
index 5e08a73..6ab3111 100644
--- a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
@@ -16,36 +16,20 @@
package androidx.heifwriter;
-import android.graphics.Bitmap;
-import android.graphics.Rect;
-import android.graphics.SurfaceTexture;
-import android.media.Image;
import android.media.MediaCodec;
-import android.media.MediaCodec.BufferInfo;
-import android.media.MediaCodec.CodecException;
import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecList;
import android.media.MediaFormat;
-import android.opengl.GLES20;
import android.os.Handler;
import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Process;
import android.util.Log;
import android.util.Range;
-import android.view.Surface;
-import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.IOException;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.concurrent.atomic.AtomicBoolean;
/**
* This class encodes images into HEIF-compatible samples using HEVC encoder.
@@ -64,115 +48,16 @@
*
* @hide
*/
-public final class HeifEncoder implements AutoCloseable,
- SurfaceTexture.OnFrameAvailableListener {
+public final class HeifEncoder extends EncoderBase {
private static final String TAG = "HeifEncoder";
private static final boolean DEBUG = false;
- private static final int GRID_WIDTH = 512;
- private static final int GRID_HEIGHT = 512;
- private static final double MAX_COMPRESS_RATIO = 0.25f;
- private static final int INPUT_BUFFER_POOL_SIZE = 2;
+ protected static final int GRID_WIDTH = 512;
+ protected static final int GRID_HEIGHT = 512;
+ protected static final double MAX_COMPRESS_RATIO = 0.25f;
private static final MediaCodecList sMCL =
- new MediaCodecList(MediaCodecList.REGULAR_CODECS);
-
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- MediaCodec mEncoder;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final Callback mCallback;
- private final HandlerThread mHandlerThread;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final Handler mHandler;
- private final @InputMode int mInputMode;
-
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final int mWidth;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final int mHeight;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final int mGridWidth;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final int mGridHeight;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final int mGridRows;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final int mGridCols;
- private final int mNumTiles;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final boolean mUseGrid;
-
- private int mInputIndex;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- boolean mInputEOS;
- private final Rect mSrcRect;
- private final Rect mDstRect;
- private ByteBuffer mCurrentBuffer;
- private final ArrayList<ByteBuffer> mEmptyBuffers = new ArrayList<>();
- private final ArrayList<ByteBuffer> mFilledBuffers = new ArrayList<>();
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final ArrayList<Integer> mCodecInputBuffers = new ArrayList<>();
-
- // Helper for tracking EOS when surface is used
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- SurfaceEOSTracker mEOSTracker;
-
- // Below variables are to handle GL copy from client's surface
- // to encoder surface when tiles are used.
- private SurfaceTexture mInputTexture;
- private Surface mInputSurface;
- private Surface mEncoderSurface;
- private EglWindowSurface mEncoderEglSurface;
- private EglRectBlt mRectBlt;
- private int mTextureId;
- private final float[] mTmpMatrix = new float[16];
- private final AtomicBoolean mStopping = new AtomicBoolean(false);
-
- public static final int INPUT_MODE_BUFFER = HeifWriter.INPUT_MODE_BUFFER;
- public static final int INPUT_MODE_SURFACE = HeifWriter.INPUT_MODE_SURFACE;
- public static final int INPUT_MODE_BITMAP = HeifWriter.INPUT_MODE_BITMAP;
- @IntDef({
- INPUT_MODE_BUFFER,
- INPUT_MODE_SURFACE,
- INPUT_MODE_BITMAP,
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface InputMode {}
-
- public static abstract class Callback {
- /**
- * Called when the output format has changed.
- *
- * @param encoder The HeifEncoder object.
- * @param format The new output format.
- */
- public abstract void onOutputFormatChanged(
- @NonNull HeifEncoder encoder, @NonNull MediaFormat format);
-
- /**
- * Called when an output buffer becomes available.
- *
- * @param encoder The HeifEncoder object.
- * @param byteBuffer the available output buffer.
- */
- public abstract void onDrainOutputBuffer(
- @NonNull HeifEncoder encoder, @NonNull ByteBuffer byteBuffer);
-
- /**
- * Called when encoding reached the end of stream without error.
- *
- * @param encoder The HeifEncoder object.
- */
- public abstract void onComplete(@NonNull HeifEncoder encoder);
-
- /**
- * Called when encoding hits an error.
- *
- * @param encoder The HeifEncoder object.
- * @param e The exception that the codec reported.
- */
- public abstract void onError(@NonNull HeifEncoder encoder, @NonNull CodecException e);
- }
+ new MediaCodecList(MediaCodecList.REGULAR_CODECS);
/**
* Configure the heif encoding session. Should only be called once.
@@ -189,198 +74,15 @@
* @param cb The callback to receive various messages from the heif encoder.
*/
public HeifEncoder(int width, int height, boolean useGrid,
- int quality, @InputMode int inputMode,
- @Nullable Handler handler, @NonNull Callback cb) throws IOException {
- if (DEBUG) Log.d(TAG, "width: " + width + ", height: " + height +
- ", useGrid: " + useGrid + ", quality: " + quality + ", inputMode: " + inputMode);
-
- if (width < 0 || height < 0 || quality < 0 || quality > 100) {
- throw new IllegalArgumentException("invalid encoder inputs");
- }
-
- // Disable grid if the image is too small
- useGrid &= (width > GRID_WIDTH || height > GRID_HEIGHT);
-
- boolean useHeicEncoder = false;
- MediaCodecInfo.CodecCapabilities caps = null;
- try {
- mEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
- caps = mEncoder.getCodecInfo().getCapabilitiesForType(
- MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
- // If the HEIC encoder can't support the size, fall back to HEVC encoder.
- if (!caps.getVideoCapabilities().isSizeSupported(width, height)) {
- mEncoder.release();
- mEncoder = null;
- throw new Exception();
- }
- useHeicEncoder = true;
- } catch (Exception e) {
- mEncoder = MediaCodec.createByCodecName(findHevcFallback());
- caps = mEncoder.getCodecInfo().getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_HEVC);
- // Always enable grid if the size is too large for the HEVC encoder
- useGrid |= !caps.getVideoCapabilities().isSizeSupported(width, height);
- }
-
- mInputMode = inputMode;
-
- mCallback = cb;
-
- Looper looper = (handler != null) ? handler.getLooper() : null;
- if (looper == null) {
- mHandlerThread = new HandlerThread("HeifEncoderThread",
- Process.THREAD_PRIORITY_FOREGROUND);
- mHandlerThread.start();
- looper = mHandlerThread.getLooper();
- } else {
- mHandlerThread = null;
- }
- mHandler = new Handler(looper);
- boolean useSurfaceInternally =
- (inputMode == INPUT_MODE_SURFACE) || (inputMode == INPUT_MODE_BITMAP);
- int colorFormat = useSurfaceInternally ? CodecCapabilities.COLOR_FormatSurface :
- CodecCapabilities.COLOR_FormatYUV420Flexible;
- boolean copyTiles = (useGrid && !useHeicEncoder) || (inputMode == INPUT_MODE_BITMAP);
-
- mWidth = width;
- mHeight = height;
- mUseGrid = useGrid;
-
- int gridWidth, gridHeight, gridRows, gridCols;
-
- if (useGrid) {
- gridWidth = GRID_WIDTH;
- gridHeight = GRID_HEIGHT;
- gridRows = (height + GRID_HEIGHT - 1) / GRID_HEIGHT;
- gridCols = (width + GRID_WIDTH - 1) / GRID_WIDTH;
- } else {
- gridWidth = mWidth;
- gridHeight = mHeight;
- gridRows = 1;
- gridCols = 1;
- }
-
- MediaFormat codecFormat;
- if (useHeicEncoder) {
- codecFormat = MediaFormat.createVideoFormat(
- MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC, mWidth, mHeight);
- } else {
- codecFormat = MediaFormat.createVideoFormat(
- MediaFormat.MIMETYPE_VIDEO_HEVC, gridWidth, gridHeight);
- }
-
- if (useGrid) {
- codecFormat.setInteger(MediaFormat.KEY_TILE_WIDTH, gridWidth);
- codecFormat.setInteger(MediaFormat.KEY_TILE_HEIGHT, gridHeight);
- codecFormat.setInteger(MediaFormat.KEY_GRID_COLUMNS, gridCols);
- codecFormat.setInteger(MediaFormat.KEY_GRID_ROWS, gridRows);
- }
-
- if (useHeicEncoder) {
- mGridWidth = width;
- mGridHeight = height;
- mGridRows = 1;
- mGridCols = 1;
- } else {
- mGridWidth = gridWidth;
- mGridHeight = gridHeight;
- mGridRows = gridRows;
- mGridCols = gridCols;
- }
- mNumTiles = mGridRows * mGridCols;
-
- codecFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 0);
- codecFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
- codecFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mNumTiles);
-
- // When we're doing tiles, set the operating rate higher as the size
- // is small, otherwise set to the normal 30fps.
- if (mNumTiles > 1) {
- codecFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, 120);
- } else {
- codecFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, 30);
- }
-
- if (useSurfaceInternally && !copyTiles) {
- // Use fixed PTS gap and disable backward frame drop
- Log.d(TAG, "Setting fixed pts gap");
- codecFormat.setLong(MediaFormat.KEY_MAX_PTS_GAP_TO_ENCODER, -1000000);
- }
-
- MediaCodecInfo.EncoderCapabilities encoderCaps = caps.getEncoderCapabilities();
-
- if (encoderCaps.isBitrateModeSupported(
- MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
- Log.d(TAG, "Setting bitrate mode to constant quality");
- Range<Integer> qualityRange = encoderCaps.getQualityRange();
- Log.d(TAG, "Quality range: " + qualityRange);
- codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
- MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ);
- codecFormat.setInteger(MediaFormat.KEY_QUALITY, (int) (qualityRange.getLower() +
- (qualityRange.getUpper() - qualityRange.getLower()) * quality / 100.0));
- } else {
- if (encoderCaps.isBitrateModeSupported(
- MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR)) {
- Log.d(TAG, "Setting bitrate mode to constant bitrate");
- codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
- MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
- } else { // assume VBR
- Log.d(TAG, "Setting bitrate mode to variable bitrate");
- codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
- MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
- }
- // Calculate the bitrate based on image dimension, max compression ratio and quality.
- // Note that we set the frame rate to the number of tiles, so the bitrate would be the
- // intended bits for one image.
- int bitrate = caps.getVideoCapabilities().getBitrateRange().clamp(
- (int) (width * height * 1.5 * 8 * MAX_COMPRESS_RATIO * quality / 100.0f));
- codecFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
- }
-
- mEncoder.setCallback(new EncoderCallback(), mHandler);
- mEncoder.configure(codecFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
-
- if (useSurfaceInternally) {
- mEncoderSurface = mEncoder.createInputSurface();
-
- mEOSTracker = new SurfaceEOSTracker(copyTiles);
-
- if (copyTiles) {
- mEncoderEglSurface = new EglWindowSurface(mEncoderSurface);
- mEncoderEglSurface.makeCurrent();
-
- mRectBlt = new EglRectBlt(
- new Texture2dProgram((inputMode == INPUT_MODE_BITMAP)
- ? Texture2dProgram.TEXTURE_2D
- : Texture2dProgram.TEXTURE_EXT),
- mWidth, mHeight);
-
- mTextureId = mRectBlt.createTextureObject();
-
- if (inputMode == INPUT_MODE_SURFACE) {
- // use single buffer mode to block on input
- mInputTexture = new SurfaceTexture(mTextureId, true);
- mInputTexture.setOnFrameAvailableListener(this);
- mInputTexture.setDefaultBufferSize(mWidth, mHeight);
- mInputSurface = new Surface(mInputTexture);
- }
-
- // make uncurrent since onFrameAvailable could be called on arbituray thread.
- // making the context current on a different thread will cause error.
- mEncoderEglSurface.makeUnCurrent();
- } else {
- mInputSurface = mEncoderSurface;
- }
- } else {
- for (int i = 0; i < INPUT_BUFFER_POOL_SIZE; i++) {
- mEmptyBuffers.add(ByteBuffer.allocateDirect(mWidth * mHeight * 3 / 2));
- }
- }
-
- mDstRect = new Rect(0, 0, mGridWidth, mGridHeight);
- mSrcRect = new Rect();
+ int quality, @InputMode int inputMode,
+ @Nullable Handler handler, @NonNull Callback cb) throws IOException {
+ super("HEIC", width, height, useGrid, quality, inputMode, handler, cb,
+ /* useBitDepth10 */ false);
+ mEncoder.setCallback(new HevcEncoderCallback(), mHandler);
+ finishSettingUpEncoder(/* useBitDepth10 */ false);
}
- private String findHevcFallback() {
+ protected static String findHevcFallback() {
String hevc = null; // first HEVC encoder
for (MediaCodecInfo info : sMCL.getCodecInfos()) {
if (!info.isEncoder()) {
@@ -396,7 +98,7 @@
continue;
}
if (caps.getEncoderCapabilities().isBitrateModeSupported(
- MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
+ MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
// Encoder that supports CQ mode is preferred over others,
// return the first encoder that supports CQ mode.
// (No need to check if it's hw based, it's already listed in
@@ -410,508 +112,12 @@
// If no encoders support CQ, return the first HEVC encoder.
return hevc;
}
- /**
- * Copies from source frame to encoder inputs using GL. The source could be either
- * client's input surface, or the input bitmap loaded to texture.
- */
- private void copyTilesGL() {
- GLES20.glViewport(0, 0, mGridWidth, mGridHeight);
-
- for (int row = 0; row < mGridRows; row++) {
- for (int col = 0; col < mGridCols; col++) {
- int left = col * mGridWidth;
- int top = row * mGridHeight;
- mSrcRect.set(left, top, left + mGridWidth, top + mGridHeight);
- try {
- mRectBlt.copyRect(mTextureId, Texture2dProgram.V_FLIP_MATRIX, mSrcRect);
- } catch (RuntimeException e) {
- // EGL copy could throw if the encoder input surface is no longer valid
- // after encoder is released. This is not an error because we're already
- // stopping (either after EOS is received or requested by client).
- if (mStopping.get()) {
- return;
- }
- throw e;
- }
- mEncoderEglSurface.setPresentationTime(
- 1000 * computePresentationTime(mInputIndex++));
- mEncoderEglSurface.swapBuffers();
- }
- }
- }
-
- @Override
- public void onFrameAvailable(SurfaceTexture surfaceTexture) {
- synchronized (this) {
- if (mEncoderEglSurface == null) {
- return;
- }
-
- mEncoderEglSurface.makeCurrent();
-
- surfaceTexture.updateTexImage();
- surfaceTexture.getTransformMatrix(mTmpMatrix);
-
- long timestampNs = surfaceTexture.getTimestamp();
-
- if (DEBUG) Log.d(TAG, "onFrameAvailable: timestampUs " + (timestampNs / 1000));
-
- boolean takeFrame = mEOSTracker.updateLastInputAndEncoderTime(timestampNs,
- computePresentationTime(mInputIndex + mNumTiles - 1));
-
- if (takeFrame) {
- copyTilesGL();
- }
-
- surfaceTexture.releaseTexImage();
-
- // make uncurrent since the onFrameAvailable could be called on arbituray thread.
- // making the context current on a different thread will cause error.
- mEncoderEglSurface.makeUnCurrent();
- }
- }
-
- /**
- * Start the encoding process.
- */
- public void start() {
- mEncoder.start();
- }
-
- /**
- * Add one YUV buffer to be encoded. This might block if the encoder can't process the input
- * buffers fast enough.
- *
- * After the call returns, the client can reuse the data array.
- *
- * @param format The YUV format as defined in {@link android.graphics.ImageFormat}, currently
- * only support YUV_420_888.
- *
- * @param data byte array containing the YUV data. If the format has more than one planes,
- * they must be concatenated.
- */
- public void addYuvBuffer(int format, @NonNull byte[] data) {
- if (mInputMode != INPUT_MODE_BUFFER) {
- throw new IllegalStateException(
- "addYuvBuffer is only allowed in buffer input mode");
- }
- if (data == null || data.length != mWidth * mHeight * 3 / 2) {
- throw new IllegalArgumentException("invalid data");
- }
- addYuvBufferInternal(data);
- }
-
- /**
- * Retrieves the input surface for encoding.
- *
- * Will only return valid value if configured to use surface input.
- */
- public @NonNull Surface getInputSurface() {
- if (mInputMode != INPUT_MODE_SURFACE) {
- throw new IllegalStateException(
- "getInputSurface is only allowed in surface input mode");
- }
- return mInputSurface;
- }
-
- /**
- * Sets the timestamp (in nano seconds) of the last input frame to encode. Frames with
- * timestamps larger than the specified value will not be encoded. However, if a frame
- * already started encoding when this is set, all tiles within that frame will be encoded.
- *
- * This method only applies when surface is used.
- */
- public void setEndOfInputStreamTimestamp(long timestampNs) {
- if (mInputMode != INPUT_MODE_SURFACE) {
- throw new IllegalStateException(
- "setEndOfInputStreamTimestamp is only allowed in surface input mode");
- }
- if (mEOSTracker != null) {
- mEOSTracker.updateInputEOSTime(timestampNs);
- }
- }
-
- /**
- * Adds one bitmap to be encoded.
- */
- public void addBitmap(@NonNull Bitmap bitmap) {
- if (mInputMode != INPUT_MODE_BITMAP) {
- throw new IllegalStateException("addBitmap is only allowed in bitmap input mode");
- }
-
- boolean takeFrame = mEOSTracker.updateLastInputAndEncoderTime(
- computePresentationTime(mInputIndex) * 1000,
- computePresentationTime(mInputIndex + mNumTiles - 1));
-
- if (!takeFrame) return;
-
- synchronized (this) {
- if (mEncoderEglSurface == null) {
- return;
- }
-
- mEncoderEglSurface.makeCurrent();
-
- mRectBlt.loadTexture(mTextureId, bitmap);
-
- copyTilesGL();
-
- // make uncurrent since the onFrameAvailable could be called on arbituray thread.
- // making the context current on a different thread will cause error.
- mEncoderEglSurface.makeUnCurrent();
- }
- }
-
- /**
- * Sends input EOS to the encoder. Result will be notified asynchronously via
- * {@link Callback#onComplete(HeifEncoder)} if encoder reaches EOS without error, or
- * {@link Callback#onError(HeifEncoder, CodecException)} otherwise.
- */
- public void stopAsync() {
- if (mInputMode == INPUT_MODE_BITMAP) {
- // here we simply set the EOS timestamp to 0, so that the cut off will be the last
- // bitmap ever added.
- mEOSTracker.updateInputEOSTime(0);
- } else if (mInputMode == INPUT_MODE_BUFFER) {
- addYuvBufferInternal(null);
- }
- }
-
- /**
- * Generates the presentation time for input frame N, in microseconds.
- * The timestamp advances 1 sec for every whole frame.
- */
- private long computePresentationTime(int frameIndex) {
- return 132 + (long)frameIndex * 1000000 / mNumTiles;
- }
-
- /**
- * Obtains one empty input buffer and copies the data into it. Before input
- * EOS is sent, this would block until the data is copied. After input EOS
- * is sent, this would return immediately.
- */
- private void addYuvBufferInternal(@Nullable byte[] data) {
- ByteBuffer buffer = acquireEmptyBuffer();
- if (buffer == null) {
- return;
- }
- buffer.clear();
- if (data != null) {
- buffer.put(data);
- }
- buffer.flip();
- synchronized (mFilledBuffers) {
- mFilledBuffers.add(buffer);
- }
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- maybeCopyOneTileYUV();
- }
- });
- }
-
- /**
- * Routine to copy one tile if we have both input and codec buffer available.
- *
- * Must be called on the handler looper that also handles the MediaCodec callback.
- */
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- void maybeCopyOneTileYUV() {
- ByteBuffer currentBuffer;
- while ((currentBuffer = getCurrentBuffer()) != null && !mCodecInputBuffers.isEmpty()) {
- int index = mCodecInputBuffers.remove(0);
-
- // 0-length input means EOS.
- boolean inputEOS = (mInputIndex % mNumTiles == 0) && (currentBuffer.remaining() == 0);
-
- if (!inputEOS) {
- Image image = mEncoder.getInputImage(index);
- int left = mGridWidth * (mInputIndex % mGridCols);
- int top = mGridHeight * (mInputIndex / mGridCols % mGridRows);
- mSrcRect.set(left, top, left + mGridWidth, top + mGridHeight);
- copyOneTileYUV(currentBuffer, image, mWidth, mHeight, mSrcRect, mDstRect);
- }
-
- mEncoder.queueInputBuffer(index, 0,
- inputEOS ? 0 : mEncoder.getInputBuffer(index).capacity(),
- computePresentationTime(mInputIndex++),
- inputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
-
- if (inputEOS || mInputIndex % mNumTiles == 0) {
- returnEmptyBufferAndNotify(inputEOS);
- }
- }
- }
-
- /**
- * Copies from a rect from src buffer to dst image.
- * TOOD: This will be replaced by JNI.
- */
- private static void copyOneTileYUV(
- ByteBuffer srcBuffer, Image dstImage,
- int srcWidth, int srcHeight,
- Rect srcRect, Rect dstRect) {
- if (srcRect.width() != dstRect.width() || srcRect.height() != dstRect.height()) {
- throw new IllegalArgumentException("src and dst rect size are different!");
- }
- if (srcWidth % 2 != 0 || srcHeight % 2 != 0 ||
- srcRect.left % 2 != 0 || srcRect.top % 2 != 0 ||
- srcRect.right % 2 != 0 || srcRect.bottom % 2 != 0 ||
- dstRect.left % 2 != 0 || dstRect.top % 2 != 0 ||
- dstRect.right % 2 != 0 || dstRect.bottom % 2 != 0) {
- throw new IllegalArgumentException("src or dst are not aligned!");
- }
-
- Image.Plane[] planes = dstImage.getPlanes();
- for (int n = 0; n < planes.length; n++) {
- ByteBuffer dstBuffer = planes[n].getBuffer();
- int colStride = planes[n].getPixelStride();
- int copyWidth = Math.min(srcRect.width(), srcWidth - srcRect.left);
- int copyHeight = Math.min(srcRect.height(), srcHeight - srcRect.top);
- int srcPlanePos = 0, div = 1;
- if (n > 0) {
- div = 2;
- srcPlanePos = srcWidth * srcHeight * (n + 3) / 4;
- }
- for (int i = 0; i < copyHeight / div; i++) {
- srcBuffer.position(srcPlanePos +
- (i + srcRect.top / div) * srcWidth / div + srcRect.left / div);
- dstBuffer.position((i + dstRect.top / div) * planes[n].getRowStride()
- + dstRect.left * colStride / div);
-
- for (int j = 0; j < copyWidth / div; j++) {
- dstBuffer.put(srcBuffer.get());
- if (colStride > 1 && j != copyWidth / div - 1) {
- dstBuffer.position(dstBuffer.position() + colStride - 1);
- }
- }
- }
- }
- }
-
- private ByteBuffer acquireEmptyBuffer() {
- synchronized (mEmptyBuffers) {
- // wait for an empty input buffer first
- while (!mInputEOS && mEmptyBuffers.isEmpty()) {
- try {
- mEmptyBuffers.wait();
- } catch (InterruptedException e) {}
- }
-
- // if already EOS, return null to stop further encoding.
- return mInputEOS ? null : mEmptyBuffers.remove(0);
- }
- }
-
- /**
- * Routine to get the current input buffer to copy from.
- * Only called on callback handler thread.
- */
- private ByteBuffer getCurrentBuffer() {
- if (!mInputEOS && mCurrentBuffer == null) {
- synchronized (mFilledBuffers) {
- mCurrentBuffer = mFilledBuffers.isEmpty() ?
- null : mFilledBuffers.remove(0);
- }
- }
- return mInputEOS ? null : mCurrentBuffer;
- }
-
- /**
- * Routine to put the consumed input buffer back into the empty buffer pool.
- * Only called on callback handler thread.
- */
- private void returnEmptyBufferAndNotify(boolean inputEOS) {
- synchronized (mEmptyBuffers) {
- mInputEOS |= inputEOS;
- mEmptyBuffers.add(mCurrentBuffer);
- mEmptyBuffers.notifyAll();
- }
- mCurrentBuffer = null;
- }
-
- /**
- * Routine to release all resources. Must be run on the same looper that
- * handles the MediaCodec callbacks.
- */
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- void stopInternal() {
- if (DEBUG) Log.d(TAG, "stopInternal");
-
- // set stopping, so that the tile copy would bail out
- // if it hits failure after this point.
- mStopping.set(true);
-
- // after start, mEncoder is only accessed on handler, so no need to sync.
- try {
- if (mEncoder != null) {
- mEncoder.stop();
- mEncoder.release();
- }
- } catch (Exception e) {
- } finally {
- mEncoder = null;
- }
-
- // unblock the addBuffer() if we're tearing down before EOS is sent.
- synchronized (mEmptyBuffers) {
- mInputEOS = true;
- mEmptyBuffers.notifyAll();
- }
-
- // Clean up surface and Egl related refs. This lock must come after encoder
- // release. When we're closing, we insert stopInternal() at the front of queue
- // so that the shutdown can be processed promptly, this means there might be
- // some output available requests queued after this. As the tile copies trying
- // to finish the current frame, there is a chance is might get stuck because
- // those outputs were not returned. Shutting down the encoder will make break
- // the tile copier out of that.
- synchronized(this) {
- try {
- if (mRectBlt != null) {
- mRectBlt.release(false);
- }
- } catch (Exception e) {
- } finally {
- mRectBlt = null;
- }
-
- try {
- if (mEncoderEglSurface != null) {
- // Note that this frees mEncoderSurface too. If mEncoderEglSurface is not
- // there, client is responsible to release the input surface it got from us,
- // we don't release mEncoderSurface here.
- mEncoderEglSurface.release();
- }
- } catch (Exception e) {
- } finally {
- mEncoderEglSurface = null;
- }
-
- try {
- if (mInputTexture != null) {
- mInputTexture.release();
- }
- } catch (Exception e) {
- } finally {
- mInputTexture = null;
- }
- }
- }
-
- /**
- * This class handles EOS for surface or bitmap inputs.
- *
- * When encoding from surface or bitmap, we can't call {@link MediaCodec#signalEndOfInputStream()}
- * immediately after input is drawn, since this could drop all pending frames in the
- * buffer queue. When there are tiles, this could leave us a partially encoded image.
- *
- * So here we track the EOS status by timestamps, and only signal EOS to the encoder
- * when we collected all images we need.
- *
- * Since this is updated from multiple threads ({@link #setEndOfInputStreamTimestamp(long)},
- * {@link EncoderCallback#onOutputBufferAvailable(MediaCodec, int, BufferInfo)},
- * {@link #addBitmap(Bitmap)} and {@link #onFrameAvailable(SurfaceTexture)}), it must be fully
- * synchronized.
- *
- * Note that when buffer input is used, the EOS flag is set in
- * {@link EncoderCallback#onInputBufferAvailable(MediaCodec, int)} and this class is not used.
- */
- private class SurfaceEOSTracker {
- private static final boolean DEBUG_EOS = false;
-
- final boolean mCopyTiles;
- long mInputEOSTimeNs = -1;
- long mLastInputTimeNs = -1;
- long mEncoderEOSTimeUs = -1;
- long mLastEncoderTimeUs = -1;
- long mLastOutputTimeUs = -1;
- boolean mSignaled;
-
- SurfaceEOSTracker(boolean copyTiles) {
- mCopyTiles = copyTiles;
- }
-
- synchronized void updateInputEOSTime(long timestampNs) {
- if (DEBUG_EOS) Log.d(TAG, "updateInputEOSTime: " + timestampNs);
-
- if (mCopyTiles) {
- if (mInputEOSTimeNs < 0) {
- mInputEOSTimeNs = timestampNs;
- }
- } else {
- if (mEncoderEOSTimeUs < 0) {
- mEncoderEOSTimeUs = timestampNs / 1000;
- }
- }
- updateEOSLocked();
- }
-
- synchronized boolean updateLastInputAndEncoderTime(long inputTimeNs, long encoderTimeUs) {
- if (DEBUG_EOS) Log.d(TAG,
- "updateLastInputAndEncoderTime: " + inputTimeNs + ", " + encoderTimeUs);
-
- boolean shouldTakeFrame = mInputEOSTimeNs < 0 || inputTimeNs <= mInputEOSTimeNs;
- if (shouldTakeFrame) {
- mLastEncoderTimeUs = encoderTimeUs;
- }
- mLastInputTimeNs = inputTimeNs;
- updateEOSLocked();
- return shouldTakeFrame;
- }
-
- synchronized void updateLastOutputTime(long outputTimeUs) {
- if (DEBUG_EOS) Log.d(TAG, "updateLastOutputTime: " + outputTimeUs);
-
- mLastOutputTimeUs = outputTimeUs;
- updateEOSLocked();
- }
-
- private void updateEOSLocked() {
- if (mSignaled) {
- return;
- }
- if (mEncoderEOSTimeUs < 0) {
- if (mInputEOSTimeNs >= 0 && mLastInputTimeNs >= mInputEOSTimeNs) {
- if (mLastEncoderTimeUs < 0) {
- doSignalEOSLocked();
- return;
- }
- // mEncoderEOSTimeUs tracks the timestamp of the last output buffer we
- // will wait for. When that buffer arrives, encoder will be signalled EOS.
- mEncoderEOSTimeUs = mLastEncoderTimeUs;
- if (DEBUG_EOS) Log.d(TAG,
- "updateEOSLocked: mEncoderEOSTimeUs " + mEncoderEOSTimeUs);
- }
- }
- if (mEncoderEOSTimeUs >= 0 && mEncoderEOSTimeUs <= mLastOutputTimeUs) {
- doSignalEOSLocked();
- }
- }
-
- private void doSignalEOSLocked() {
- if (DEBUG_EOS) Log.d(TAG, "doSignalEOSLocked");
-
- mHandler.post(new Runnable() {
- @Override public void run() {
- if (mEncoder != null) {
- mEncoder.signalEndOfInputStream();
- }
- }
- });
-
- mSignaled = true;
- }
- }
/**
* MediaCodec callback for HEVC encoding.
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
- class EncoderCallback extends MediaCodec.Callback {
- private boolean mOutputEOS;
-
+ protected class HevcEncoderCallback extends EncoderCallback {
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
if (codec != mEncoder) return;
@@ -919,7 +125,7 @@
if (DEBUG) Log.d(TAG, "onOutputFormatChanged: " + format);
if (!MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC.equals(
- format.getString(MediaFormat.KEY_MIME))) {
+ format.getString(MediaFormat.KEY_MIME))) {
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
format.setInteger(MediaFormat.KEY_WIDTH, mWidth);
format.setInteger(MediaFormat.KEY_HEIGHT, mHeight);
@@ -934,85 +140,5 @@
mCallback.onOutputFormatChanged(HeifEncoder.this, format);
}
-
- @Override
- public void onInputBufferAvailable(MediaCodec codec, int index) {
- if (codec != mEncoder || mInputEOS) return;
-
- if (DEBUG) Log.d(TAG, "onInputBufferAvailable: " + index);
- mCodecInputBuffers.add(index);
- maybeCopyOneTileYUV();
- }
-
- @Override
- public void onOutputBufferAvailable(MediaCodec codec, int index, BufferInfo info) {
- if (codec != mEncoder || mOutputEOS) return;
-
- if (DEBUG) {
- Log.d(TAG, "onOutputBufferAvailable: " + index
- + ", time " + info.presentationTimeUs
- + ", size " + info.size
- + ", flags " + info.flags);
- }
-
- if ((info.size > 0) && ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0)) {
- ByteBuffer outputBuffer = codec.getOutputBuffer(index);
-
- // reset position as addBuffer() modifies it
- outputBuffer.position(info.offset);
- outputBuffer.limit(info.offset + info.size);
-
- if (mEOSTracker != null) {
- mEOSTracker.updateLastOutputTime(info.presentationTimeUs);
- }
-
- mCallback.onDrainOutputBuffer(HeifEncoder.this, outputBuffer);
- }
-
- mOutputEOS |= ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0);
-
- codec.releaseOutputBuffer(index, false);
-
- if (mOutputEOS) {
- stopAndNotify(null);
- }
- }
-
- @Override
- public void onError(MediaCodec codec, CodecException e) {
- if (codec != mEncoder) return;
-
- Log.e(TAG, "onError: " + e);
- stopAndNotify(e);
- }
-
- private void stopAndNotify(@Nullable CodecException e) {
- stopInternal();
- if (e == null) {
- mCallback.onComplete(HeifEncoder.this);
- } else {
- mCallback.onError(HeifEncoder.this, e);
- }
- }
}
-
- @Override
- public void close() {
- // unblock the addBuffer() if we're tearing down before EOS is sent.
- synchronized (mEmptyBuffers) {
- mInputEOS = true;
- mEmptyBuffers.notifyAll();
- }
-
- mHandler.postAtFrontOfQueue(new Runnable() {
- @Override
- public void run() {
- try {
- stopInternal();
- } catch (Exception e) {
- // We don't want to crash when closing.
- }
- }
- });
- }
-}
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
index 978654a..878b1ac 100644
--- a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
@@ -32,6 +32,7 @@
import android.view.Surface;
import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -77,42 +78,17 @@
*
* <p>Please refer to the documentations on individual methods for the exact usage.
*/
-public final class HeifWriter implements AutoCloseable {
+@SuppressWarnings("HiddenSuperclass")
+public final class HeifWriter extends WriterBase {
private static final String TAG = "HeifWriter";
private static final boolean DEBUG = false;
- private static final int MUXER_DATA_FLAG = 16;
-
- private final @InputMode int mInputMode;
- private final HandlerThread mHandlerThread;
- private final Handler mHandler;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- int mNumTiles;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final int mRotation;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final int mMaxImages;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final int mPrimaryIndex;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final ResultWaiter mResultWaiter = new ResultWaiter();
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- MediaMuxer mMuxer;
- private HeifEncoder mHeifEncoder;
- final AtomicBoolean mMuxerStarted = new AtomicBoolean(false);
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- int[] mTrackIndexArray;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- int mOutputIndex;
- private boolean mStarted;
-
- private final List<Pair<Integer, ByteBuffer>> mExifList = new ArrayList<>();
/**
* The input mode where the client adds input buffers with YUV data.
*
* @see #addYuvBuffer(int, byte[])
*/
- public static final int INPUT_MODE_BUFFER = 0;
+ public static final int INPUT_MODE_BUFFER = WriterBase.INPUT_MODE_BUFFER;
/**
* The input mode where the client renders the images to an input Surface
@@ -125,18 +101,18 @@
*
* @see #getInputSurface()
*/
- public static final int INPUT_MODE_SURFACE = 1;
+ public static final int INPUT_MODE_SURFACE = WriterBase.INPUT_MODE_SURFACE;
/**
* The input mode where the client adds bitmaps.
*
* @see #addBitmap(Bitmap)
*/
- public static final int INPUT_MODE_BITMAP = 2;
+ public static final int INPUT_MODE_BITMAP = WriterBase.INPUT_MODE_BITMAP;
/** @hide */
@IntDef({
- INPUT_MODE_BUFFER, INPUT_MODE_SURFACE, INPUT_MODE_BITMAP,
+ INPUT_MODE_BUFFER, INPUT_MODE_SURFACE, INPUT_MODE_BITMAP,
})
@Retention(RetentionPolicy.SOURCE)
public @interface InputMode {}
@@ -161,13 +137,15 @@
* Construct a Builder with output specified by its path.
*
* @param path Path of the file to be written.
- * @param width Width of the image.
- * @param height Height of the image.
+ * @param width Width of the image in number of pixels.
+ * @param height Height of the image in number of pixels.
* @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
* {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
*/
public Builder(@NonNull String path,
- int width, int height, @InputMode int inputMode) {
+ @IntRange(from = 1) int width,
+ @IntRange(from = 1) int height,
+ @InputMode int inputMode) {
this(path, null, width, height, inputMode);
}
@@ -175,21 +153,22 @@
* Construct a Builder with output specified by its file descriptor.
*
* @param fd File descriptor of the file to be written.
- * @param width Width of the image.
- * @param height Height of the image.
+ * @param width Width of the image in number of pixels.
+ * @param height Height of the image in number of pixels.
* @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
* {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
*/
public Builder(@NonNull FileDescriptor fd,
- int width, int height, @InputMode int inputMode) {
+ @IntRange(from = 1) int width,
+ @IntRange(from = 1) int height,
+ @InputMode int inputMode) {
this(null, fd, width, height, inputMode);
}
private Builder(String path, FileDescriptor fd,
- int width, int height, @InputMode int inputMode) {
- if (width <= 0 || height <= 0) {
- throw new IllegalArgumentException("Invalid image size: " + width + "x" + height);
- }
+ @IntRange(from = 1) int width,
+ @IntRange(from = 1) int height,
+ @InputMode int inputMode) {
mPath = path;
mFd = fd;
mWidth = width;
@@ -200,11 +179,11 @@
/**
* Set the image rotation in degrees.
*
- * @param rotation Rotation angle (clockwise) of the image, must be 0, 90, 180 or 270.
- * Default is 0.
+ * @param rotation Rotation angle in degrees (clockwise) of the image, must be 0, 90,
+ * 180 or 270. Default is 0.
* @return this Builder object.
*/
- public Builder setRotation(int rotation) {
+ public @NonNull Builder setRotation(@IntRange(from = 0) int rotation) {
if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270) {
throw new IllegalArgumentException("Invalid rotation angle: " + rotation);
}
@@ -219,7 +198,7 @@
* automatically chosen. Default is to enable.
* @return this Builder object.
*/
- public Builder setGridEnabled(boolean gridEnabled) {
+ public @NonNull Builder setGridEnabled(boolean gridEnabled) {
mGridEnabled = gridEnabled;
return this;
}
@@ -231,7 +210,7 @@
* quality supported by this implementation. Default is 100.
* @return this Builder object.
*/
- public Builder setQuality(int quality) {
+ public @NonNull Builder setQuality(@IntRange(from = 0, to = 100) int quality) {
if (quality < 0 || quality > 100) {
throw new IllegalArgumentException("Invalid quality: " + quality);
}
@@ -250,7 +229,7 @@
* Default is 1.
* @return this Builder object.
*/
- public Builder setMaxImages(int maxImages) {
+ public @NonNull Builder setMaxImages(@IntRange(from = 1) int maxImages) {
if (maxImages <= 0) {
throw new IllegalArgumentException("Invalid maxImage: " + maxImages);
}
@@ -265,10 +244,7 @@
* range [0, maxImages - 1] inclusive. Default is 0.
* @return this Builder object.
*/
- public Builder setPrimaryIndex(int primaryIndex) {
- if (primaryIndex < 0) {
- throw new IllegalArgumentException("Invalid primaryIndex: " + primaryIndex);
- }
+ public @NonNull Builder setPrimaryIndex(@IntRange(from = 0) int primaryIndex) {
mPrimaryIndex = primaryIndex;
return this;
}
@@ -281,7 +257,7 @@
* writer. Default is null.
* @return this Builder object.
*/
- public Builder setHandler(@Nullable Handler handler) {
+ public @NonNull Builder setHandler(@Nullable Handler handler) {
mHandler = handler;
return this;
}
@@ -293,428 +269,46 @@
* @throws IOException if failed to create the writer, possibly due to failure to create
* {@link android.media.MediaMuxer} or {@link android.media.MediaCodec}.
*/
- public HeifWriter build() throws IOException {
+ public @NonNull HeifWriter build() throws IOException {
return new HeifWriter(mPath, mFd, mWidth, mHeight, mRotation, mGridEnabled, mQuality,
- mMaxImages, mPrimaryIndex, mInputMode, mHandler);
+ mMaxImages, mPrimaryIndex, mInputMode, mHandler);
}
}
@SuppressLint("WrongConstant")
@SuppressWarnings("WeakerAccess") /* synthetic access */
HeifWriter(@NonNull String path,
- @NonNull FileDescriptor fd,
- int width,
- int height,
- int rotation,
- boolean gridEnabled,
- int quality,
- int maxImages,
- int primaryIndex,
- @InputMode int inputMode,
- @Nullable Handler handler) throws IOException {
- if (primaryIndex >= maxImages) {
- throw new IllegalArgumentException(
- "Invalid maxImages (" + maxImages + ") or primaryIndex (" + primaryIndex + ")");
- }
+ @NonNull FileDescriptor fd,
+ int width,
+ int height,
+ int rotation,
+ boolean gridEnabled,
+ int quality,
+ int maxImages,
+ int primaryIndex,
+ @InputMode int inputMode,
+ @Nullable Handler handler) throws IOException {
+ super(rotation, inputMode, maxImages, primaryIndex, gridEnabled, quality,
+ handler, /* highBitDepthEnabled */ false);
if (DEBUG) {
Log.d(TAG, "width: " + width
- + ", height: " + height
- + ", rotation: " + rotation
- + ", gridEnabled: " + gridEnabled
- + ", quality: " + quality
- + ", maxImages: " + maxImages
- + ", primaryIndex: " + primaryIndex
- + ", inputMode: " + inputMode);
+ + ", height: " + height
+ + ", rotation: " + rotation
+ + ", gridEnabled: " + gridEnabled
+ + ", quality: " + quality
+ + ", maxImages: " + maxImages
+ + ", primaryIndex: " + primaryIndex
+ + ", inputMode: " + inputMode);
}
// set to 1 initially, and wait for output format to know for sure
mNumTiles = 1;
- mRotation = rotation;
- mInputMode = inputMode;
- mMaxImages = maxImages;
- mPrimaryIndex = primaryIndex;
-
- Looper looper = (handler != null) ? handler.getLooper() : null;
- if (looper == null) {
- mHandlerThread = new HandlerThread("HeifEncoderThread",
- Process.THREAD_PRIORITY_FOREGROUND);
- mHandlerThread.start();
- looper = mHandlerThread.getLooper();
- } else {
- mHandlerThread = null;
- }
- mHandler = new Handler(looper);
-
mMuxer = (path != null) ? new MediaMuxer(path, MUXER_OUTPUT_HEIF)
- : new MediaMuxer(fd, MUXER_OUTPUT_HEIF);
+ : new MediaMuxer(fd, MUXER_OUTPUT_HEIF);
- mHeifEncoder = new HeifEncoder(width, height, gridEnabled, quality,
- mInputMode, mHandler, new HeifCallback());
+ mEncoder = new HeifEncoder(width, height, gridEnabled, quality,
+ mInputMode, mHandler, new WriterCallback());
}
-
- /**
- * Start the heif writer. Can only be called once.
- *
- * @throws IllegalStateException if called more than once.
- */
- public void start() {
- checkStarted(false);
- mStarted = true;
- mHeifEncoder.start();
- }
-
- /**
- * Add one YUV buffer to the heif file.
- *
- * @param format The YUV format as defined in {@link android.graphics.ImageFormat}, currently
- * only support YUV_420_888.
- *
- * @param data byte array containing the YUV data. If the format has more than one planes,
- * they must be concatenated.
- *
- * @throws IllegalStateException if not started or not configured to use buffer input.
- */
- public void addYuvBuffer(int format, @NonNull byte[] data) {
- checkStartedAndMode(INPUT_MODE_BUFFER);
- synchronized (this) {
- if (mHeifEncoder != null) {
- mHeifEncoder.addYuvBuffer(format, data);
- }
- }
- }
-
- /**
- * Retrieves the input surface for encoding.
- *
- * @return the input surface if configured to use surface input.
- *
- * @throws IllegalStateException if called after start or not configured to use surface input.
- */
- public @NonNull Surface getInputSurface() {
- checkStarted(false);
- checkMode(INPUT_MODE_SURFACE);
- return mHeifEncoder.getInputSurface();
- }
-
- /**
- * Set the timestamp (in nano seconds) of the last input frame to encode.
- *
- * This call is only valid for surface input. Client can use this to stop the heif writer
- * earlier before the maximum number of images are written. If not called, the writer will
- * only stop when the maximum number of images are written.
- *
- * @param timestampNs timestamp (in nano seconds) of the last frame that will be written to the
- * heif file. Frames with timestamps larger than the specified value will not
- * be written. However, if a frame already started encoding when this is set,
- * all tiles within that frame will be encoded.
- *
- * @throws IllegalStateException if not started or not configured to use surface input.
- */
- public void setInputEndOfStreamTimestamp(long timestampNs) {
- checkStartedAndMode(INPUT_MODE_SURFACE);
- synchronized (this) {
- if (mHeifEncoder != null) {
- mHeifEncoder.setEndOfInputStreamTimestamp(timestampNs);
- }
- }
- }
-
- /**
- * Add one bitmap to the heif file.
- *
- * @param bitmap the bitmap to be added to the file.
- * @throws IllegalStateException if not started or not configured to use bitmap input.
- */
- public void addBitmap(@NonNull Bitmap bitmap) {
- checkStartedAndMode(INPUT_MODE_BITMAP);
- synchronized (this) {
- if (mHeifEncoder != null) {
- mHeifEncoder.addBitmap(bitmap);
- }
- }
- }
-
- /**
- * Add Exif data for the specified image. The data must be a valid Exif data block,
- * starting with "Exif\0\0" followed by the TIFF header (See JEITA CP-3451C Section 4.5.2.)
- *
- * @param imageIndex index of the image, must be a valid index for the max number of image
- * specified by {@link Builder#setMaxImages(int)}.
- * @param exifData byte buffer containing a Exif data block.
- * @param offset offset of the Exif data block within exifData.
- * @param length length of the Exif data block.
- */
- public void addExifData(int imageIndex, @NonNull byte[] exifData, int offset, int length) {
- checkStarted(true);
-
- ByteBuffer buffer = ByteBuffer.allocateDirect(length);
- buffer.put(exifData, offset, length);
- buffer.flip();
- // Put it in a queue, as we might not be able to process it at this time.
- synchronized (mExifList) {
- mExifList.add(new Pair<Integer, ByteBuffer>(imageIndex, buffer));
- }
- processExifData();
- }
-
- @SuppressLint("WrongConstant")
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- void processExifData() {
- if (!mMuxerStarted.get()) {
- return;
- }
-
- while (true) {
- Pair<Integer, ByteBuffer> entry;
- synchronized (mExifList) {
- if (mExifList.isEmpty()) {
- return;
- }
- entry = mExifList.remove(0);
- }
- MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
- info.set(entry.second.position(), entry.second.remaining(), 0, MUXER_DATA_FLAG);
- mMuxer.writeSampleData(mTrackIndexArray[entry.first], entry.second, info);
- }
- }
-
- /**
- * Stop the heif writer synchronously. Throws exception if the writer didn't finish writing
- * successfully. Upon a success return:
- *
- * - For buffer and bitmap inputs, all images sent before stop will be written.
- *
- * - For surface input, images with timestamp on or before that specified in
- * {@link #setInputEndOfStreamTimestamp(long)} will be written. In case where
- * {@link #setInputEndOfStreamTimestamp(long)} was never called, stop will block
- * until maximum number of images are received.
- *
- * @param timeoutMs Maximum time (in microsec) to wait for the writer to complete, with zero
- * indicating waiting indefinitely.
- * @see #setInputEndOfStreamTimestamp(long)
- * @throws Exception if encountered error, in which case the output file may not be valid. In
- * particular, {@link TimeoutException} is thrown when timed out, and {@link
- * MediaCodec.CodecException} is thrown when encountered codec error.
- */
- public void stop(long timeoutMs) throws Exception {
- checkStarted(true);
- synchronized (this) {
- if (mHeifEncoder != null) {
- mHeifEncoder.stopAsync();
- }
- }
- mResultWaiter.waitForResult(timeoutMs);
- processExifData();
- closeInternal();
- }
-
- private void checkStarted(boolean requiredStarted) {
- if (mStarted != requiredStarted) {
- throw new IllegalStateException("Already started");
- }
- }
-
- private void checkMode(@InputMode int requiredMode) {
- if (mInputMode != requiredMode) {
- throw new IllegalStateException("Not valid in input mode " + mInputMode);
- }
- }
-
- private void checkStartedAndMode(@InputMode int requiredMode) {
- checkStarted(true);
- checkMode(requiredMode);
- }
-
- /**
- * Routine to stop and release writer, must be called on the same looper
- * that receives heif encoder callbacks.
- */
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- void closeInternal() {
- if (DEBUG) Log.d(TAG, "closeInternal");
- // We don't want to crash when closing, catch all exceptions.
- try {
- // Muxer could throw exceptions if stop is called without samples.
- // Don't crash in that case.
- if (mMuxer != null) {
- mMuxer.stop();
- mMuxer.release();
- }
- } catch (Exception e) {
- } finally {
- mMuxer = null;
- }
- try {
- if (mHeifEncoder != null) {
- mHeifEncoder.close();
- }
- } catch (Exception e) {
- } finally {
- synchronized (this) {
- mHeifEncoder = null;
- }
- }
- }
-
- /**
- * Callback from the heif encoder.
- */
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- class HeifCallback extends HeifEncoder.Callback {
- private boolean mEncoderStopped;
- /**
- * Upon receiving output format from the encoder, add the requested number of
- * image tracks to the muxer and start the muxer.
- */
- @Override
- public void onOutputFormatChanged(
- @NonNull HeifEncoder encoder, @NonNull MediaFormat format) {
- if (mEncoderStopped) return;
-
- if (DEBUG) {
- Log.d(TAG, "onOutputFormatChanged: " + format);
- }
- if (mTrackIndexArray != null) {
- stopAndNotify(new IllegalStateException(
- "Output format changed after muxer started"));
- return;
- }
-
- try {
- int gridRows = format.getInteger(MediaFormat.KEY_GRID_ROWS);
- int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLUMNS);
- mNumTiles = gridRows * gridCols;
- } catch (NullPointerException | ClassCastException e) {
- mNumTiles = 1;
- }
-
- // add mMaxImages image tracks of the same format
- mTrackIndexArray = new int[mMaxImages];
-
- // set rotation angle
- if (mRotation > 0) {
- Log.d(TAG, "setting rotation: " + mRotation);
- mMuxer.setOrientationHint(mRotation);
- }
- for (int i = 0; i < mTrackIndexArray.length; i++) {
- // mark primary
- format.setInteger(MediaFormat.KEY_IS_DEFAULT, (i == mPrimaryIndex) ? 1 : 0);
- mTrackIndexArray[i] = mMuxer.addTrack(format);
- }
- mMuxer.start();
- mMuxerStarted.set(true);
- processExifData();
- }
-
- /**
- * Upon receiving an output buffer from the encoder (which is one image when
- * grid is not used, or one tile if grid is used), add that sample to the muxer.
- */
- @Override
- public void onDrainOutputBuffer(
- @NonNull HeifEncoder encoder, @NonNull ByteBuffer byteBuffer) {
- if (mEncoderStopped) return;
-
- if (DEBUG) {
- Log.d(TAG, "onDrainOutputBuffer: " + mOutputIndex);
- }
- if (mTrackIndexArray == null) {
- stopAndNotify(new IllegalStateException(
- "Output buffer received before format info"));
- return;
- }
-
- if (mOutputIndex < mMaxImages * mNumTiles) {
- MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
- info.set(byteBuffer.position(), byteBuffer.remaining(), 0, 0);
- mMuxer.writeSampleData(
- mTrackIndexArray[mOutputIndex / mNumTiles], byteBuffer, info);
- }
-
- mOutputIndex++;
-
- // post EOS if reached max number of images allowed.
- if (mOutputIndex == mMaxImages * mNumTiles) {
- stopAndNotify(null);
- }
- }
-
- @Override
- public void onComplete(@NonNull HeifEncoder encoder) {
- stopAndNotify(null);
- }
-
- @Override
- public void onError(@NonNull HeifEncoder encoder, @NonNull MediaCodec.CodecException e) {
- stopAndNotify(e);
- }
-
- private void stopAndNotify(@Nullable Exception error) {
- if (mEncoderStopped) return;
-
- mEncoderStopped = true;
- mResultWaiter.signalResult(error);
- }
- }
-
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- static class ResultWaiter {
- private boolean mDone;
- private Exception mException;
-
- synchronized void waitForResult(long timeoutMs) throws Exception {
- if (timeoutMs < 0) {
- throw new IllegalArgumentException("timeoutMs is negative");
- }
- if (timeoutMs == 0) {
- while (!mDone) {
- try {
- wait();
- } catch (InterruptedException ex) {}
- }
- } else {
- final long startTimeMs = System.currentTimeMillis();
- long remainingWaitTimeMs = timeoutMs;
- // avoid early termination by "spurious" wakeup.
- while (!mDone && remainingWaitTimeMs > 0) {
- try {
- wait(remainingWaitTimeMs);
- } catch (InterruptedException ex) {}
- remainingWaitTimeMs -= (System.currentTimeMillis() - startTimeMs);
- }
- }
- if (!mDone) {
- mDone = true;
- mException = new TimeoutException("timed out waiting for result");
- }
- if (mException != null) {
- throw mException;
- }
- }
-
- synchronized void signalResult(@Nullable Exception e) {
- if (!mDone) {
- mDone = true;
- mException = e;
- notifyAll();
- }
- }
- }
-
- @Override
- public void close() {
- mHandler.postAtFrontOfQueue(new Runnable() {
- @Override
- public void run() {
- try {
- closeInternal();
- } catch (Exception e) {
- // If the client called stop() properly, any errors would have been
- // reported there. We don't want to crash when closing.
- }
- }
- });
- }
-}
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/WriterBase.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/WriterBase.java
new file mode 100644
index 0000000..7f283edf
--- /dev/null
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/WriterBase.java
@@ -0,0 +1,572 @@
+/*
+ * Copyright 2022 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.heifwriter;
+
+import static android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_HEIF;
+
+import android.annotation.SuppressLint;
+import android.graphics.Bitmap;
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+import android.media.MediaMuxer;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Process;
+import android.util.Log;
+import android.util.Pair;
+import android.view.Surface;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * This class holds common utliities for {@link HeifWriter} and {@link AvifWriter}.
+ *
+ * @hide
+ */
+public class WriterBase implements AutoCloseable {
+ private static final String TAG = "WriterBase";
+ private static final boolean DEBUG = false;
+ private static final int MUXER_DATA_FLAG = 16;
+
+ /**
+ * The input mode where the client adds input buffers with YUV data.
+ *
+ * @see #addYuvBuffer(int, byte[])
+ */
+ protected static final int INPUT_MODE_BUFFER = 0;
+
+ /**
+ * The input mode where the client renders the images to an input Surface
+ * created by the writer.
+ *
+ * The input surface operates in single buffer mode. As a result, for use case
+ * where camera directly outputs to the input surface, this mode will not work
+ * because camera framework requires multiple buffers to operate in a pipeline
+ * fashion.
+ *
+ * @see #getInputSurface()
+ */
+ protected static final int INPUT_MODE_SURFACE = 1;
+
+ /**
+ * The input mode where the client adds bitmaps.
+ *
+ * @see #addBitmap(Bitmap)
+ */
+ protected static final int INPUT_MODE_BITMAP = 2;
+
+ /** @hide */
+ @IntDef({
+ INPUT_MODE_BUFFER, INPUT_MODE_SURFACE, INPUT_MODE_BITMAP,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface InputMode {}
+
+ protected final @InputMode int mInputMode;
+ protected final boolean mHighBitDepthEnabled;
+ protected final HandlerThread mHandlerThread;
+ protected final Handler mHandler;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ protected int mNumTiles;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ protected final int mRotation;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ protected final int mMaxImages;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ protected final int mPrimaryIndex;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final ResultWaiter mResultWaiter = new ResultWaiter();
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ @NonNull protected MediaMuxer mMuxer;
+ @NonNull protected EncoderBase mEncoder;
+ final AtomicBoolean mMuxerStarted = new AtomicBoolean(false);
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ int[] mTrackIndexArray;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ int mOutputIndex;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ boolean mGridEnabled;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ int mQuality;
+ private boolean mStarted;
+
+ private final List<Pair<Integer, ByteBuffer>> mExifList = new ArrayList<>();
+
+ protected WriterBase(int rotation,
+ @InputMode int inputMode,
+ int maxImages,
+ int primaryIndex,
+ boolean gridEnabled,
+ int quality,
+ @Nullable Handler handler,
+ boolean highBitDepthEnabled) throws IOException {
+ if (primaryIndex >= maxImages) {
+ throw new IllegalArgumentException(
+ "Invalid maxImages (" + maxImages + ") or primaryIndex (" + primaryIndex + ")");
+ }
+
+ mRotation = rotation;
+ mInputMode = inputMode;
+ mMaxImages = maxImages;
+ mPrimaryIndex = primaryIndex;
+ mGridEnabled = gridEnabled;
+ mQuality = quality;
+ mHighBitDepthEnabled = highBitDepthEnabled;
+
+ Looper looper = (handler != null) ? handler.getLooper() : null;
+ if (looper == null) {
+ mHandlerThread = new HandlerThread("HeifEncoderThread",
+ Process.THREAD_PRIORITY_FOREGROUND);
+ mHandlerThread.start();
+ looper = mHandlerThread.getLooper();
+ } else {
+ mHandlerThread = null;
+ }
+ mHandler = new Handler(looper);
+ }
+
+ /**
+ * Start the heif writer. Can only be called once.
+ *
+ * @throws IllegalStateException if called more than once.
+ */
+ public void start() {
+ checkStarted(false);
+ mStarted = true;
+ mEncoder.start();
+ }
+
+ /**
+ * Add one YUV buffer to the heif file.
+ *
+ * @param format The YUV format as defined in {@link android.graphics.ImageFormat}, currently
+ * only support YUV_420_888.
+ *
+ * @param data byte array containing the YUV data. If the format has more than one planes,
+ * they must be concatenated.
+ *
+ * @throws IllegalStateException if not started or not configured to use buffer input.
+ */
+ public void addYuvBuffer(int format, @NonNull byte[] data) {
+ checkStartedAndMode(INPUT_MODE_BUFFER);
+ synchronized (this) {
+ if (mEncoder != null) {
+ mEncoder.addYuvBuffer(format, data);
+ }
+ }
+ }
+
+ /**
+ * Retrieves the input surface for encoding.
+ *
+ * @return the input surface if configured to use surface input.
+ *
+ * @throws IllegalStateException if called after start or not configured to use surface input.
+ */
+ public @NonNull Surface getInputSurface() {
+ checkStarted(false);
+ checkMode(INPUT_MODE_SURFACE);
+ return mEncoder.getInputSurface();
+ }
+
+ /**
+ * Set the timestamp (in nano seconds) of the last input frame to encode.
+ *
+ * This call is only valid for surface input. Client can use this to stop the heif writer
+ * earlier before the maximum number of images are written. If not called, the writer will
+ * only stop when the maximum number of images are written.
+ *
+ * @param timestampNs timestamp (in nano seconds) of the last frame that will be written to the
+ * heif file. Frames with timestamps larger than the specified value will not
+ * be written. However, if a frame already started encoding when this is set,
+ * all tiles within that frame will be encoded.
+ *
+ * @throws IllegalStateException if not started or not configured to use surface input.
+ */
+ public void setInputEndOfStreamTimestamp(@IntRange(from = 0) long timestampNs) {
+ checkStartedAndMode(INPUT_MODE_SURFACE);
+ synchronized (this) {
+ if (mEncoder != null) {
+ mEncoder.setEndOfInputStreamTimestamp(timestampNs);
+ }
+ }
+ }
+
+ /**
+ * Add one bitmap to the heif file.
+ *
+ * @param bitmap the bitmap to be added to the file.
+ * @throws IllegalStateException if not started or not configured to use bitmap input.
+ */
+ public void addBitmap(@NonNull Bitmap bitmap) {
+ checkStartedAndMode(INPUT_MODE_BITMAP);
+ synchronized (this) {
+ if (mEncoder != null) {
+ mEncoder.addBitmap(bitmap);
+ }
+ }
+ }
+
+ /**
+ * Add Exif data for the specified image. The data must be a valid Exif data block,
+ * starting with "Exif\0\0" followed by the TIFF header (See JEITA CP-3451C Section 4.5.2.)
+ *
+ * @param imageIndex index of the image, must be a valid index for the max number of image
+ * specified by {@link Builder#setMaxImages(int)}.
+ * @param exifData byte buffer containing a Exif data block.
+ * @param offset offset of the Exif data block within exifData.
+ * @param length length of the Exif data block.
+ */
+ public void addExifData(int imageIndex, @NonNull byte[] exifData, int offset, int length) {
+ checkStarted(true);
+
+ ByteBuffer buffer = ByteBuffer.allocateDirect(length);
+ buffer.put(exifData, offset, length);
+ buffer.flip();
+ // Put it in a queue, as we might not be able to process it at this time.
+ synchronized (mExifList) {
+ mExifList.add(new Pair<Integer, ByteBuffer>(imageIndex, buffer));
+ }
+ processExifData();
+ }
+
+ @SuppressLint("WrongConstant")
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ void processExifData() {
+ if (!mMuxerStarted.get()) {
+ return;
+ }
+
+ while (true) {
+ Pair<Integer, ByteBuffer> entry;
+ synchronized (mExifList) {
+ if (mExifList.isEmpty()) {
+ return;
+ }
+ entry = mExifList.remove(0);
+ }
+ MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
+ info.set(entry.second.position(), entry.second.remaining(), 0, MUXER_DATA_FLAG);
+ mMuxer.writeSampleData(mTrackIndexArray[entry.first], entry.second, info);
+ }
+ }
+
+ /**
+ * Stop the heif writer synchronously. Throws exception if the writer didn't finish writing
+ * successfully. Upon a success return:
+ *
+ * - For buffer and bitmap inputs, all images sent before stop will be written.
+ *
+ * - For surface input, images with timestamp on or before that specified in
+ * {@link #setInputEndOfStreamTimestamp(long)} will be written. In case where
+ * {@link #setInputEndOfStreamTimestamp(long)} was never called, stop will block
+ * until maximum number of images are received.
+ *
+ * @param timeoutMs Maximum time (in microsec) to wait for the writer to complete, with zero
+ * indicating waiting indefinitely.
+ * @see #setInputEndOfStreamTimestamp(long)
+ * @throws Exception if encountered error, in which case the output file may not be valid. In
+ * particular, {@link TimeoutException} is thrown when timed out, and {@link
+ * MediaCodec.CodecException} is thrown when encountered codec error.
+ */
+ public void stop(@IntRange(from = 0) long timeoutMs) throws Exception {
+ checkStarted(true);
+ synchronized (this) {
+ if (mEncoder != null) {
+ mEncoder.stopAsync();
+ }
+ }
+ mResultWaiter.waitForResult(timeoutMs);
+ processExifData();
+ closeInternal();
+ }
+
+ private void checkStarted(boolean requiredStarted) {
+ if (mStarted != requiredStarted) {
+ throw new IllegalStateException("Already started");
+ }
+ }
+
+ private void checkMode(@InputMode int requiredMode) {
+ if (mInputMode != requiredMode) {
+ throw new IllegalStateException("Not valid in input mode " + mInputMode);
+ }
+ }
+
+ private void checkStartedAndMode(@InputMode int requiredMode) {
+ checkStarted(true);
+ checkMode(requiredMode);
+ }
+
+ /**
+ * Routine to stop and release writer, must be called on the same looper
+ * that receives heif encoder callbacks.
+ */
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ void closeInternal() {
+ if (DEBUG) Log.d(TAG, "closeInternal");
+ // We don't want to crash when closing, catch all exceptions.
+ try {
+ // Muxer could throw exceptions if stop is called without samples.
+ // Don't crash in that case.
+ if (mMuxer != null) {
+ mMuxer.stop();
+ mMuxer.release();
+ }
+ } catch (Exception e) {
+ } finally {
+ mMuxer = null;
+ }
+ try {
+ if (mEncoder != null) {
+ mEncoder.close();
+ }
+ } catch (Exception e) {
+ } finally {
+ synchronized (this) {
+ mEncoder = null;
+ }
+ }
+ }
+
+ /**
+ * Callback from the encoder.
+ */
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ protected class WriterCallback extends EncoderBase.Callback {
+ private boolean mEncoderStopped;
+ /**
+ * Upon receiving output format from the encoder, add the requested number of
+ * image tracks to the muxer and start the muxer.
+ */
+ @Override
+ public void onOutputFormatChanged(
+ @NonNull EncoderBase encoder, @NonNull MediaFormat format) {
+ if (mEncoderStopped) return;
+
+ if (DEBUG) {
+ Log.d(TAG, "onOutputFormatChanged: " + format);
+ }
+ if (mTrackIndexArray != null) {
+ stopAndNotify(new IllegalStateException(
+ "Output format changed after muxer started"));
+ return;
+ }
+
+ try {
+ int gridRows = format.getInteger(MediaFormat.KEY_GRID_ROWS);
+ int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLUMNS);
+ mNumTiles = gridRows * gridCols;
+ } catch (NullPointerException | ClassCastException e) {
+ mNumTiles = 1;
+ }
+
+ // add mMaxImages image tracks of the same format
+ mTrackIndexArray = new int[mMaxImages];
+
+ // set rotation angle
+ if (mRotation > 0) {
+ Log.d(TAG, "setting rotation: " + mRotation);
+ mMuxer.setOrientationHint(mRotation);
+ }
+ for (int i = 0; i < mTrackIndexArray.length; i++) {
+ // mark primary
+ format.setInteger(MediaFormat.KEY_IS_DEFAULT, (i == mPrimaryIndex) ? 1 : 0);
+ mTrackIndexArray[i] = mMuxer.addTrack(format);
+ }
+ mMuxer.start();
+ mMuxerStarted.set(true);
+ processExifData();
+ }
+
+ /**
+ * Upon receiving an output buffer from the encoder (which is one image when
+ * grid is not used, or one tile if grid is used), add that sample to the muxer.
+ */
+ @Override
+ public void onDrainOutputBuffer(
+ @NonNull EncoderBase encoder, @NonNull ByteBuffer byteBuffer) {
+ if (mEncoderStopped) return;
+
+ if (DEBUG) {
+ Log.d(TAG, "onDrainOutputBuffer: " + mOutputIndex);
+ }
+ if (mTrackIndexArray == null) {
+ stopAndNotify(new IllegalStateException(
+ "Output buffer received before format info"));
+ return;
+ }
+
+ if (mOutputIndex < mMaxImages * mNumTiles) {
+ MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
+ info.set(byteBuffer.position(), byteBuffer.remaining(), 0, 0);
+ mMuxer.writeSampleData(
+ mTrackIndexArray[mOutputIndex / mNumTiles], byteBuffer, info);
+ }
+
+ mOutputIndex++;
+
+ // post EOS if reached max number of images allowed.
+ if (mOutputIndex == mMaxImages * mNumTiles) {
+ stopAndNotify(null);
+ }
+ }
+
+ @Override
+ public void onComplete(@NonNull EncoderBase encoder) {
+ stopAndNotify(null);
+ }
+
+ @Override
+ public void onError(@NonNull EncoderBase encoder, @NonNull MediaCodec.CodecException e) {
+ stopAndNotify(e);
+ }
+
+ private void stopAndNotify(@Nullable Exception error) {
+ if (mEncoderStopped) return;
+
+ mEncoderStopped = true;
+ mResultWaiter.signalResult(error);
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ static class ResultWaiter {
+ private boolean mDone;
+ private Exception mException;
+
+ synchronized void waitForResult(long timeoutMs) throws Exception {
+ if (timeoutMs < 0) {
+ throw new IllegalArgumentException("timeoutMs is negative");
+ }
+ if (timeoutMs == 0) {
+ while (!mDone) {
+ try {
+ wait();
+ } catch (InterruptedException ex) {}
+ }
+ } else {
+ final long startTimeMs = System.currentTimeMillis();
+ long remainingWaitTimeMs = timeoutMs;
+ // avoid early termination by "spurious" wakeup.
+ while (!mDone && remainingWaitTimeMs > 0) {
+ try {
+ wait(remainingWaitTimeMs);
+ } catch (InterruptedException ex) {}
+ remainingWaitTimeMs -= (System.currentTimeMillis() - startTimeMs);
+ }
+ }
+ if (!mDone) {
+ mDone = true;
+ mException = new TimeoutException("timed out waiting for result");
+ }
+ if (mException != null) {
+ throw mException;
+ }
+ }
+
+ synchronized void signalResult(@Nullable Exception e) {
+ if (!mDone) {
+ mDone = true;
+ mException = e;
+ notifyAll();
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ mHandler.postAtFrontOfQueue(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ closeInternal();
+ } catch (Exception e) {
+ // If the client called stop() properly, any errors would have been
+ // reported there. We don't want to crash when closing.
+ }
+ }
+ });
+ }
+
+ /*
+ * Gets rotation.
+ */
+ public int getRotation() {
+ return mRotation;
+ }
+
+ /*
+ * Returns true if grid is enabled.
+ */
+ public boolean isGridEnabled() {
+ return mGridEnabled;
+ }
+
+ /*
+ * Gets configured quality.
+ */
+ public int getQuality() {
+ return mQuality;
+ }
+
+ /*
+ * Gets number of maximum images.
+ */
+ public int getMaxImages() {
+ return mMaxImages;
+ }
+
+ /*
+ * Gets index of the primary image.
+ */
+ public int getPrimaryIndex() {
+ return mPrimaryIndex;
+ }
+
+ /*
+ * Gets handler.
+ *
+ * The result is the same as clients' input from setHandler() method.
+ * If not null, client will receive all callbacks on the handler's looper.
+ * Otherwise, client will receive callbacks on the current looper.
+ */
+ public @Nullable Handler getHandler() {
+ return mHandler;
+ }
+
+ /*
+ * Returns true if high bit-depth is enabled.
+ */
+ public boolean isHighBitDepthEnabled() {
+ return mHighBitDepthEnabled;
+ }
+}
\ No newline at end of file
diff --git a/hilt/hilt-common/build.gradle b/hilt/hilt-common/build.gradle
index 24537d9..4737307 100644
--- a/hilt/hilt-common/build.gradle
+++ b/hilt/hilt-common/build.gradle
@@ -27,7 +27,7 @@
}
androidx {
- name = "AndroidX Hilt Extension Annotations"
+ name = "Hilt Common"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.HILT
inceptionYear = "2020"
diff --git a/hilt/hilt-compiler/build.gradle b/hilt/hilt-compiler/build.gradle
index eb22d1c..e6b2f0e 100644
--- a/hilt/hilt-compiler/build.gradle
+++ b/hilt/hilt-compiler/build.gradle
@@ -52,7 +52,7 @@
}
androidx {
- name = "AndroidX Hilt Extension Compiler"
+ name = "Hilt Extension Compiler"
type = LibraryType.ANNOTATION_PROCESSOR
mavenVersion = LibraryVersions.HILT
inceptionYear = "2020"
diff --git a/hilt/hilt-navigation-fragment/build.gradle b/hilt/hilt-navigation-fragment/build.gradle
index 22eea96..814229f 100644
--- a/hilt/hilt-navigation-fragment/build.gradle
+++ b/hilt/hilt-navigation-fragment/build.gradle
@@ -56,7 +56,7 @@
}
androidx {
- name = "Android Navigation Fragment Hilt Extension"
+ name = "Navigation Fragment Hilt Extension"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.HILT
inceptionYear = "2021"
diff --git a/hilt/hilt-navigation/build.gradle b/hilt/hilt-navigation/build.gradle
index ad4e41a..6539c86 100644
--- a/hilt/hilt-navigation/build.gradle
+++ b/hilt/hilt-navigation/build.gradle
@@ -32,7 +32,7 @@
}
androidx {
- name = "Android Navigation Hilt Extension"
+ name = "Navigation Hilt Extension"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.HILT
inceptionYear = "2021"
diff --git a/hilt/hilt-work/build.gradle b/hilt/hilt-work/build.gradle
index 46f258b..abe1186 100644
--- a/hilt/hilt-work/build.gradle
+++ b/hilt/hilt-work/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "Android Lifecycle WorkManager Hilt Extension"
+ name = "WorkManager Hilt Extension"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.HILT
inceptionYear = "2020"
diff --git a/input/input-motionprediction/build.gradle b/input/input-motionprediction/build.gradle
index c630796..e5e6f105 100644
--- a/input/input-motionprediction/build.gradle
+++ b/input/input-motionprediction/build.gradle
@@ -39,7 +39,7 @@
}
androidx {
- name = "Android Motion Prediction"
+ name = "Motion Prediction"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.INPUT_MOTIONPREDICTION
inceptionYear = "2022"
diff --git a/inspection/inspection-gradle-plugin/build.gradle b/inspection/inspection-gradle-plugin/build.gradle
index 4589093..bddd9ff 100644
--- a/inspection/inspection-gradle-plugin/build.gradle
+++ b/inspection/inspection-gradle-plugin/build.gradle
@@ -52,7 +52,7 @@
}
androidx {
- name = "Android Inspection Gradle Plugin"
+ name = "Inspection Gradle Plugin"
type = LibraryType.GRADLE_PLUGIN
publish = Publish.NONE
inceptionYear = "2019"
diff --git a/inspection/inspection-testing/build.gradle b/inspection/inspection-testing/build.gradle
index 7f51b57..f7b000e 100644
--- a/inspection/inspection-testing/build.gradle
+++ b/inspection/inspection-testing/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "AndroidX Inspection Testing"
+ name = "Inspection Testing"
type = LibraryType.INTERNAL_TEST_LIBRARY
publish = Publish.NONE
inceptionYear = "2019"
diff --git a/inspection/inspection/build.gradle b/inspection/inspection/build.gradle
index 5c35b71..a142722 100644
--- a/inspection/inspection/build.gradle
+++ b/inspection/inspection/build.gradle
@@ -34,7 +34,7 @@
}
androidx {
- name = "AndroidX Inspection"
+ name = "Inspection"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
description = "Experimental AndroidX Inspection Project"
diff --git a/interpolator/interpolator/build.gradle b/interpolator/interpolator/build.gradle
index 524e14e..00cd919 100644
--- a/interpolator/interpolator/build.gradle
+++ b/interpolator/interpolator/build.gradle
@@ -10,7 +10,7 @@
}
androidx {
- name = "Android Support Library Interpolators"
+ name = "Interpolators"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/leanback/leanback-grid/build.gradle b/leanback/leanback-grid/build.gradle
index 36a1965..b6171c8 100644
--- a/leanback/leanback-grid/build.gradle
+++ b/leanback/leanback-grid/build.gradle
@@ -52,7 +52,7 @@
}
androidx {
- name = "AndroidX Leanback Grid"
+ name = "Leanback Grid"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.LEANBACK_GRID
inceptionYear = "2021"
diff --git a/leanback/leanback-paging/build.gradle b/leanback/leanback-paging/build.gradle
index 6c51eec..0b19679 100644
--- a/leanback/leanback-paging/build.gradle
+++ b/leanback/leanback-paging/build.gradle
@@ -52,7 +52,7 @@
}
androidx {
- name = "AndroidX Leanback Paging"
+ name = "Leanback Paging"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.LEANBACK_PAGING
inceptionYear = "2020"
diff --git a/leanback/leanback-preference/build.gradle b/leanback/leanback-preference/build.gradle
index d64a337..8af0e9d 100644
--- a/leanback/leanback-preference/build.gradle
+++ b/leanback/leanback-preference/build.gradle
@@ -28,7 +28,7 @@
}
androidx {
- name = "AndroidX Leanback Preference"
+ name = "Leanback Preference"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.LEANBACK_PREFERENCE
inceptionYear = "2015"
diff --git a/leanback/leanback-tab/build.gradle b/leanback/leanback-tab/build.gradle
index 1c0b1cb..b38819a 100644
--- a/leanback/leanback-tab/build.gradle
+++ b/leanback/leanback-tab/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "AndroidX Leanback Tab"
+ name = "Leanback Tab"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.LEANBACK_TAB
inceptionYear = "2020"
diff --git a/leanback/leanback/api/api_lint.ignore b/leanback/leanback/api/api_lint.ignore
index a72d2ae..a568a48 100644
--- a/leanback/leanback/api/api_lint.ignore
+++ b/leanback/leanback/api/api_lint.ignore
@@ -147,8 +147,6 @@
Invalid nullability on parameter `view` in method `onViewCreated`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
InvalidNullabilityOverride: androidx.leanback.widget.GuidedActionEditText#onTouchEvent(android.view.MotionEvent) parameter #0:
Invalid nullability on parameter `event` in method `onTouchEvent`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.leanback.widget.ShadowOverlayContainer#draw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `canvas` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
KotlinOperator: androidx.leanback.widget.ObjectAdapter#get(int):
@@ -1135,6 +1133,8 @@
Missing nullability on field `TOP_FRACTION` in class `class androidx.leanback.graphics.CompositeDrawable.ChildDrawable`
MissingNullability: androidx.leanback.graphics.FitWidthBitmapDrawable#PROPERTY_VERTICAL_OFFSET:
Missing nullability on field `PROPERTY_VERTICAL_OFFSET` in class `class androidx.leanback.graphics.FitWidthBitmapDrawable`
+MissingNullability: androidx.leanback.graphics.FitWidthBitmapDrawable#draw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `draw`
MissingNullability: androidx.leanback.graphics.FitWidthBitmapDrawable#getBitmap():
Missing nullability on method `getBitmap` return
MissingNullability: androidx.leanback.graphics.FitWidthBitmapDrawable#getConstantState():
@@ -2189,6 +2189,8 @@
Missing nullability on parameter `context` in method `ShadowOverlayContainer`
MissingNullability: androidx.leanback.widget.ShadowOverlayContainer#ShadowOverlayContainer(android.content.Context, android.util.AttributeSet, int) parameter #1:
Missing nullability on parameter `attrs` in method `ShadowOverlayContainer`
+MissingNullability: androidx.leanback.widget.ShadowOverlayContainer#draw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `draw`
MissingNullability: androidx.leanback.widget.ShadowOverlayContainer#getWrappedView():
Missing nullability on method `getWrappedView` return
MissingNullability: androidx.leanback.widget.ShadowOverlayContainer#prepareParentForShadow(android.view.ViewGroup) parameter #0:
diff --git a/leanback/leanback/build.gradle b/leanback/leanback/build.gradle
index 1a3152a..d22458a 100644
--- a/leanback/leanback/build.gradle
+++ b/leanback/leanback/build.gradle
@@ -55,7 +55,7 @@
}
androidx {
- name = "Android Support Leanback v17"
+ name = "Leanback"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.LEANBACK
inceptionYear = "2014"
diff --git a/libraryversions.toml b/libraryversions.toml
index 413f3f3..d40bf1d 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,5 +1,5 @@
[versions]
-ACTIVITY = "1.8.0-alpha02"
+ACTIVITY = "1.8.0-alpha04"
ANNOTATION = "1.7.0-alpha03"
ANNOTATION_EXPERIMENTAL = "1.4.0-alpha01"
APPACTIONS_BUILTINTYPES = "1.0.0-alpha01"
@@ -29,7 +29,7 @@
CONSTRAINTLAYOUT_CORE = "1.1.0-alpha10"
CONTENTPAGER = "1.1.0-alpha01"
COORDINATORLAYOUT = "1.3.0-alpha01"
-CORE = "1.11.0-alpha04"
+CORE = "1.12.0-alpha04"
CORE_ANIMATION = "1.0.0-beta02"
CORE_ANIMATION_TESTING = "1.0.0-beta01"
CORE_APPDIGEST = "1.0.0-alpha01"
@@ -40,8 +40,9 @@
CORE_REMOTEVIEWS = "1.0.0-beta04"
CORE_ROLE = "1.2.0-alpha01"
CORE_SPLASHSCREEN = "1.1.0-alpha01"
+CORE_TELECOM = "1.0.0-alpha01"
CORE_UWB = "1.0.0-alpha06"
-CREDENTIALS = "1.0.0-alpha08"
+CREDENTIALS = "1.2.0-alpha04"
CURSORADAPTER = "1.1.0-alpha01"
CUSTOMVIEW = "1.2.0-alpha03"
CUSTOMVIEW_POOLINGCONTAINER = "1.1.0-alpha01"
@@ -62,6 +63,7 @@
GLANCE_TEMPLATE = "1.0.0-alpha06"
GLANCE_WEAR_TILES = "1.0.0-alpha06"
GRAPHICS_CORE = "1.0.0-alpha04"
+GRAPHICS_PATH = "1.0.0-alpha02"
GRAPHICS_FILTERS = "1.0.0-alpha01"
GRAPHICS_SHAPES = "1.0.0-alpha03"
GRIDLAYOUT = "1.1.0-beta01"
@@ -86,7 +88,7 @@
LOADER = "1.2.0-alpha01"
MEDIA = "1.7.0-alpha02"
MEDIA2 = "1.3.0-alpha01"
-MEDIAROUTER = "1.5.0-alpha01"
+MEDIAROUTER = "1.6.0-alpha03"
METRICS = "1.0.0-alpha05"
NAVIGATION = "2.7.0-alpha01"
PAGING = "3.2.0-alpha05"
diff --git a/lifecycle/lifecycle-common-java8/api/2.6.0-beta02.txt b/lifecycle/lifecycle-common-java8/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/lifecycle/lifecycle-common-java8/api/2.6.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/lifecycle/lifecycle-common-java8/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-common-java8/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/lifecycle/lifecycle-common-java8/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/lifecycle/lifecycle-common-java8/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-common-java8/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/lifecycle/lifecycle-common-java8/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/lifecycle/lifecycle-common-java8/build.gradle b/lifecycle/lifecycle-common-java8/build.gradle
index cbab42e..88e4bc1 100644
--- a/lifecycle/lifecycle-common-java8/build.gradle
+++ b/lifecycle/lifecycle-common-java8/build.gradle
@@ -30,7 +30,7 @@
}
androidx {
- name = "Android Lifecycle-Common for Java 8 Language"
+ name = "Lifecycle-Common for Java 8"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Lifecycle-Common for Java 8 Language"
diff --git a/lifecycle/lifecycle-common/api/2.6.0-beta02.txt b/lifecycle/lifecycle-common/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..f3dc4c9
--- /dev/null
+++ b/lifecycle/lifecycle-common/api/2.6.0-beta02.txt
@@ -0,0 +1,99 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public interface DefaultLifecycleObserver extends androidx.lifecycle.LifecycleObserver {
+ method public default void onCreate(androidx.lifecycle.LifecycleOwner owner);
+ method public default void onDestroy(androidx.lifecycle.LifecycleOwner owner);
+ method public default void onPause(androidx.lifecycle.LifecycleOwner owner);
+ method public default void onResume(androidx.lifecycle.LifecycleOwner owner);
+ method public default void onStart(androidx.lifecycle.LifecycleOwner owner);
+ method public default void onStop(androidx.lifecycle.LifecycleOwner owner);
+ }
+
+ public abstract class Lifecycle {
+ ctor public Lifecycle();
+ method @MainThread public abstract void addObserver(androidx.lifecycle.LifecycleObserver observer);
+ method @MainThread public abstract androidx.lifecycle.Lifecycle.State getCurrentState();
+ method @MainThread public abstract void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+ property @MainThread public abstract androidx.lifecycle.Lifecycle.State currentState;
+ }
+
+ public enum Lifecycle.Event {
+ method public static final androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+ method public static final androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+ method public final androidx.lifecycle.Lifecycle.State getTargetState();
+ method public static final androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+ method public static final androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
+ method public static androidx.lifecycle.Lifecycle.Event valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.lifecycle.Lifecycle.Event[] values();
+ property public final androidx.lifecycle.Lifecycle.State targetState;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_ANY;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_CREATE;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_DESTROY;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_PAUSE;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_RESUME;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_START;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_STOP;
+ field public static final androidx.lifecycle.Lifecycle.Event.Companion Companion;
+ }
+
+ public static final class Lifecycle.Event.Companion {
+ method public androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
+ }
+
+ public enum Lifecycle.State {
+ method public final boolean isAtLeast(androidx.lifecycle.Lifecycle.State state);
+ method public static androidx.lifecycle.Lifecycle.State valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.lifecycle.Lifecycle.State[] values();
+ enum_constant public static final androidx.lifecycle.Lifecycle.State CREATED;
+ enum_constant public static final androidx.lifecycle.Lifecycle.State DESTROYED;
+ enum_constant public static final androidx.lifecycle.Lifecycle.State INITIALIZED;
+ enum_constant public static final androidx.lifecycle.Lifecycle.State RESUMED;
+ enum_constant public static final androidx.lifecycle.Lifecycle.State STARTED;
+ }
+
+ public abstract class LifecycleCoroutineScope implements kotlinx.coroutines.CoroutineScope {
+ method @Deprecated public final kotlinx.coroutines.Job launchWhenCreated(kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method @Deprecated public final kotlinx.coroutines.Job launchWhenResumed(kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method @Deprecated public final kotlinx.coroutines.Job launchWhenStarted(kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ }
+
+ public fun interface LifecycleEventObserver extends androidx.lifecycle.LifecycleObserver {
+ method public void onStateChanged(androidx.lifecycle.LifecycleOwner source, androidx.lifecycle.Lifecycle.Event event);
+ }
+
+ public final class LifecycleKt {
+ method public static androidx.lifecycle.LifecycleCoroutineScope getCoroutineScope(androidx.lifecycle.Lifecycle);
+ }
+
+ public interface LifecycleObserver {
+ }
+
+ public interface LifecycleOwner {
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ property public abstract androidx.lifecycle.Lifecycle lifecycle;
+ }
+
+ public final class LifecycleOwnerKt {
+ method public static androidx.lifecycle.LifecycleCoroutineScope getLifecycleScope(androidx.lifecycle.LifecycleOwner);
+ }
+
+ @Deprecated @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target(java.lang.annotation.ElementType.METHOD) public @interface OnLifecycleEvent {
+ method @Deprecated public abstract androidx.lifecycle.Lifecycle.Event! value();
+ }
+
+ public final class PausingDispatcherKt {
+ method @Deprecated public static suspend <T> Object? whenCreated(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenCreated(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenResumed(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenResumed(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenStarted(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenStarted(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenStateAtLeast(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State minState, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-common/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-common/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..f3dc4c9
--- /dev/null
+++ b/lifecycle/lifecycle-common/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,99 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public interface DefaultLifecycleObserver extends androidx.lifecycle.LifecycleObserver {
+ method public default void onCreate(androidx.lifecycle.LifecycleOwner owner);
+ method public default void onDestroy(androidx.lifecycle.LifecycleOwner owner);
+ method public default void onPause(androidx.lifecycle.LifecycleOwner owner);
+ method public default void onResume(androidx.lifecycle.LifecycleOwner owner);
+ method public default void onStart(androidx.lifecycle.LifecycleOwner owner);
+ method public default void onStop(androidx.lifecycle.LifecycleOwner owner);
+ }
+
+ public abstract class Lifecycle {
+ ctor public Lifecycle();
+ method @MainThread public abstract void addObserver(androidx.lifecycle.LifecycleObserver observer);
+ method @MainThread public abstract androidx.lifecycle.Lifecycle.State getCurrentState();
+ method @MainThread public abstract void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+ property @MainThread public abstract androidx.lifecycle.Lifecycle.State currentState;
+ }
+
+ public enum Lifecycle.Event {
+ method public static final androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+ method public static final androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+ method public final androidx.lifecycle.Lifecycle.State getTargetState();
+ method public static final androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+ method public static final androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
+ method public static androidx.lifecycle.Lifecycle.Event valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.lifecycle.Lifecycle.Event[] values();
+ property public final androidx.lifecycle.Lifecycle.State targetState;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_ANY;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_CREATE;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_DESTROY;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_PAUSE;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_RESUME;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_START;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_STOP;
+ field public static final androidx.lifecycle.Lifecycle.Event.Companion Companion;
+ }
+
+ public static final class Lifecycle.Event.Companion {
+ method public androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
+ }
+
+ public enum Lifecycle.State {
+ method public final boolean isAtLeast(androidx.lifecycle.Lifecycle.State state);
+ method public static androidx.lifecycle.Lifecycle.State valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.lifecycle.Lifecycle.State[] values();
+ enum_constant public static final androidx.lifecycle.Lifecycle.State CREATED;
+ enum_constant public static final androidx.lifecycle.Lifecycle.State DESTROYED;
+ enum_constant public static final androidx.lifecycle.Lifecycle.State INITIALIZED;
+ enum_constant public static final androidx.lifecycle.Lifecycle.State RESUMED;
+ enum_constant public static final androidx.lifecycle.Lifecycle.State STARTED;
+ }
+
+ public abstract class LifecycleCoroutineScope implements kotlinx.coroutines.CoroutineScope {
+ method @Deprecated public final kotlinx.coroutines.Job launchWhenCreated(kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method @Deprecated public final kotlinx.coroutines.Job launchWhenResumed(kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method @Deprecated public final kotlinx.coroutines.Job launchWhenStarted(kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ }
+
+ public fun interface LifecycleEventObserver extends androidx.lifecycle.LifecycleObserver {
+ method public void onStateChanged(androidx.lifecycle.LifecycleOwner source, androidx.lifecycle.Lifecycle.Event event);
+ }
+
+ public final class LifecycleKt {
+ method public static androidx.lifecycle.LifecycleCoroutineScope getCoroutineScope(androidx.lifecycle.Lifecycle);
+ }
+
+ public interface LifecycleObserver {
+ }
+
+ public interface LifecycleOwner {
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ property public abstract androidx.lifecycle.Lifecycle lifecycle;
+ }
+
+ public final class LifecycleOwnerKt {
+ method public static androidx.lifecycle.LifecycleCoroutineScope getLifecycleScope(androidx.lifecycle.LifecycleOwner);
+ }
+
+ @Deprecated @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target(java.lang.annotation.ElementType.METHOD) public @interface OnLifecycleEvent {
+ method @Deprecated public abstract androidx.lifecycle.Lifecycle.Event! value();
+ }
+
+ public final class PausingDispatcherKt {
+ method @Deprecated public static suspend <T> Object? whenCreated(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenCreated(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenResumed(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenResumed(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenStarted(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenStarted(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenStateAtLeast(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State minState, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-common/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-common/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..05b2709
--- /dev/null
+++ b/lifecycle/lifecycle-common/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,116 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public interface DefaultLifecycleObserver extends androidx.lifecycle.LifecycleObserver {
+ method public default void onCreate(androidx.lifecycle.LifecycleOwner owner);
+ method public default void onDestroy(androidx.lifecycle.LifecycleOwner owner);
+ method public default void onPause(androidx.lifecycle.LifecycleOwner owner);
+ method public default void onResume(androidx.lifecycle.LifecycleOwner owner);
+ method public default void onStart(androidx.lifecycle.LifecycleOwner owner);
+ method public default void onStop(androidx.lifecycle.LifecycleOwner owner);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface GeneratedAdapter {
+ method public void callMethods(androidx.lifecycle.LifecycleOwner source, androidx.lifecycle.Lifecycle.Event event, boolean onAny, androidx.lifecycle.MethodCallsLogger? logger);
+ }
+
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface GenericLifecycleObserver extends androidx.lifecycle.LifecycleEventObserver {
+ }
+
+ public abstract class Lifecycle {
+ ctor public Lifecycle();
+ method @MainThread public abstract void addObserver(androidx.lifecycle.LifecycleObserver observer);
+ method @MainThread public abstract androidx.lifecycle.Lifecycle.State getCurrentState();
+ method @MainThread public abstract void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+ property @MainThread public abstract androidx.lifecycle.Lifecycle.State currentState;
+ }
+
+ public enum Lifecycle.Event {
+ method public static final androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+ method public static final androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+ method public final androidx.lifecycle.Lifecycle.State getTargetState();
+ method public static final androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+ method public static final androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
+ method public static androidx.lifecycle.Lifecycle.Event valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.lifecycle.Lifecycle.Event[] values();
+ property public final androidx.lifecycle.Lifecycle.State targetState;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_ANY;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_CREATE;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_DESTROY;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_PAUSE;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_RESUME;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_START;
+ enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_STOP;
+ field public static final androidx.lifecycle.Lifecycle.Event.Companion Companion;
+ }
+
+ public static final class Lifecycle.Event.Companion {
+ method public androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
+ }
+
+ public enum Lifecycle.State {
+ method public final boolean isAtLeast(androidx.lifecycle.Lifecycle.State state);
+ method public static androidx.lifecycle.Lifecycle.State valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.lifecycle.Lifecycle.State[] values();
+ enum_constant public static final androidx.lifecycle.Lifecycle.State CREATED;
+ enum_constant public static final androidx.lifecycle.Lifecycle.State DESTROYED;
+ enum_constant public static final androidx.lifecycle.Lifecycle.State INITIALIZED;
+ enum_constant public static final androidx.lifecycle.Lifecycle.State RESUMED;
+ enum_constant public static final androidx.lifecycle.Lifecycle.State STARTED;
+ }
+
+ public abstract class LifecycleCoroutineScope implements kotlinx.coroutines.CoroutineScope {
+ method @Deprecated public final kotlinx.coroutines.Job launchWhenCreated(kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method @Deprecated public final kotlinx.coroutines.Job launchWhenResumed(kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method @Deprecated public final kotlinx.coroutines.Job launchWhenStarted(kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ }
+
+ public fun interface LifecycleEventObserver extends androidx.lifecycle.LifecycleObserver {
+ method public void onStateChanged(androidx.lifecycle.LifecycleOwner source, androidx.lifecycle.Lifecycle.Event event);
+ }
+
+ public final class LifecycleKt {
+ method public static androidx.lifecycle.LifecycleCoroutineScope getCoroutineScope(androidx.lifecycle.Lifecycle);
+ }
+
+ public interface LifecycleObserver {
+ }
+
+ public interface LifecycleOwner {
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ property public abstract androidx.lifecycle.Lifecycle lifecycle;
+ }
+
+ public final class LifecycleOwnerKt {
+ method public static androidx.lifecycle.LifecycleCoroutineScope getLifecycleScope(androidx.lifecycle.LifecycleOwner);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Lifecycling {
+ method public static String getAdapterName(String className);
+ method public static androidx.lifecycle.LifecycleEventObserver lifecycleEventObserver(Object object);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class MethodCallsLogger {
+ ctor public MethodCallsLogger();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean approveCall(String name, int type);
+ }
+
+ @Deprecated @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target(java.lang.annotation.ElementType.METHOD) public @interface OnLifecycleEvent {
+ method @Deprecated public abstract androidx.lifecycle.Lifecycle.Event! value();
+ }
+
+ public final class PausingDispatcherKt {
+ method @Deprecated public static suspend <T> Object? whenCreated(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenCreated(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenResumed(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenResumed(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenStarted(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenStarted(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method @Deprecated public static suspend <T> Object? whenStateAtLeast(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State minState, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-common/build.gradle b/lifecycle/lifecycle-common/build.gradle
index fcd8ba4..1aeacc3 100644
--- a/lifecycle/lifecycle-common/build.gradle
+++ b/lifecycle/lifecycle-common/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "Android Lifecycle-Common"
+ name = "Lifecycle-Common"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Lifecycle-Common"
diff --git a/lifecycle/lifecycle-compiler/build.gradle b/lifecycle/lifecycle-compiler/build.gradle
index 60945b2..b5ac1f6 100644
--- a/lifecycle/lifecycle-compiler/build.gradle
+++ b/lifecycle/lifecycle-compiler/build.gradle
@@ -53,7 +53,7 @@
}
androidx {
- name = "Android Lifecycles Compiler"
+ name = "Lifecycles Compiler"
type = LibraryType.ANNOTATION_PROCESSOR
inceptionYear = "2017"
description = "Android Lifecycles annotation processor"
diff --git a/lifecycle/lifecycle-extensions/api/2.6.0-beta02.txt b/lifecycle/lifecycle-extensions/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..88798d8
--- /dev/null
+++ b/lifecycle/lifecycle-extensions/api/2.6.0-beta02.txt
@@ -0,0 +1,22 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ @Deprecated public class ViewModelProviders {
+ ctor @Deprecated public ViewModelProviders();
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelProvider of(androidx.fragment.app.Fragment);
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelProvider of(androidx.fragment.app.FragmentActivity);
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelProvider of(androidx.fragment.app.Fragment, androidx.lifecycle.ViewModelProvider.Factory?);
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelProvider of(androidx.fragment.app.FragmentActivity, androidx.lifecycle.ViewModelProvider.Factory?);
+ }
+
+ @Deprecated public static class ViewModelProviders.DefaultFactory extends androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory {
+ ctor @Deprecated public ViewModelProviders.DefaultFactory(android.app.Application);
+ }
+
+ @Deprecated public class ViewModelStores {
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelStore of(androidx.fragment.app.FragmentActivity);
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelStore of(androidx.fragment.app.Fragment);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-extensions/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-extensions/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..88798d8
--- /dev/null
+++ b/lifecycle/lifecycle-extensions/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,22 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ @Deprecated public class ViewModelProviders {
+ ctor @Deprecated public ViewModelProviders();
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelProvider of(androidx.fragment.app.Fragment);
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelProvider of(androidx.fragment.app.FragmentActivity);
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelProvider of(androidx.fragment.app.Fragment, androidx.lifecycle.ViewModelProvider.Factory?);
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelProvider of(androidx.fragment.app.FragmentActivity, androidx.lifecycle.ViewModelProvider.Factory?);
+ }
+
+ @Deprecated public static class ViewModelProviders.DefaultFactory extends androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory {
+ ctor @Deprecated public ViewModelProviders.DefaultFactory(android.app.Application);
+ }
+
+ @Deprecated public class ViewModelStores {
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelStore of(androidx.fragment.app.FragmentActivity);
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelStore of(androidx.fragment.app.Fragment);
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-extensions/api/res-2.6.0-beta02.txt
similarity index 100%
rename from webkit/webkit/api/res-1.6.0-beta02.txt
rename to lifecycle/lifecycle-extensions/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-extensions/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-extensions/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..88798d8
--- /dev/null
+++ b/lifecycle/lifecycle-extensions/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,22 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ @Deprecated public class ViewModelProviders {
+ ctor @Deprecated public ViewModelProviders();
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelProvider of(androidx.fragment.app.Fragment);
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelProvider of(androidx.fragment.app.FragmentActivity);
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelProvider of(androidx.fragment.app.Fragment, androidx.lifecycle.ViewModelProvider.Factory?);
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelProvider of(androidx.fragment.app.FragmentActivity, androidx.lifecycle.ViewModelProvider.Factory?);
+ }
+
+ @Deprecated public static class ViewModelProviders.DefaultFactory extends androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory {
+ ctor @Deprecated public ViewModelProviders.DefaultFactory(android.app.Application);
+ }
+
+ @Deprecated public class ViewModelStores {
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelStore of(androidx.fragment.app.FragmentActivity);
+ method @Deprecated @MainThread public static androidx.lifecycle.ViewModelStore of(androidx.fragment.app.Fragment);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-extensions/build.gradle b/lifecycle/lifecycle-extensions/build.gradle
index b729d31..b8c4e04 100644
--- a/lifecycle/lifecycle-extensions/build.gradle
+++ b/lifecycle/lifecycle-extensions/build.gradle
@@ -51,7 +51,7 @@
}
androidx {
- name = "Android Lifecycle Extensions"
+ name = "Lifecycle Extensions"
publish = Publish.NONE
runApiTasks = new RunApiTasks.Yes("Need to track API surface before moving to publish")
inceptionYear = "2017"
diff --git a/lifecycle/lifecycle-livedata-core-ktx-lint/build.gradle b/lifecycle/lifecycle-livedata-core-ktx-lint/build.gradle
index 6bb81f4..59c894a 100644
--- a/lifecycle/lifecycle-livedata-core-ktx-lint/build.gradle
+++ b/lifecycle/lifecycle-livedata-core-ktx-lint/build.gradle
@@ -34,7 +34,7 @@
}
androidx {
- name = "Android LiveData Kotlin Lint Checks"
+ name = "LiveData Kotlin Lint Checks"
type = LibraryType.LINT
inceptionYear = "2019"
description = "Lint Checks for LiveData Kotlin Extensions"
diff --git a/lifecycle/lifecycle-livedata-core-ktx/api/2.6.0-beta02.txt b/lifecycle/lifecycle-livedata-core-ktx/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..daac648
--- /dev/null
+++ b/lifecycle/lifecycle-livedata-core-ktx/api/2.6.0-beta02.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class LiveDataKt {
+ method @Deprecated @MainThread public static inline <T> androidx.lifecycle.Observer<T> observe(androidx.lifecycle.LiveData<T>, androidx.lifecycle.LifecycleOwner owner, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> onChanged);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-livedata-core-ktx/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-livedata-core-ktx/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..daac648
--- /dev/null
+++ b/lifecycle/lifecycle-livedata-core-ktx/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class LiveDataKt {
+ method @Deprecated @MainThread public static inline <T> androidx.lifecycle.Observer<T> observe(androidx.lifecycle.LiveData<T>, androidx.lifecycle.LifecycleOwner owner, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> onChanged);
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-livedata-core-ktx/api/res-2.6.0-beta02.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to lifecycle/lifecycle-livedata-core-ktx/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-livedata-core-ktx/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-livedata-core-ktx/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..daac648
--- /dev/null
+++ b/lifecycle/lifecycle-livedata-core-ktx/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class LiveDataKt {
+ method @Deprecated @MainThread public static inline <T> androidx.lifecycle.Observer<T> observe(androidx.lifecycle.LiveData<T>, androidx.lifecycle.LifecycleOwner owner, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> onChanged);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-livedata-core/api/2.6.0-beta02.txt b/lifecycle/lifecycle-livedata-core/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..f528b4e
--- /dev/null
+++ b/lifecycle/lifecycle-livedata-core/api/2.6.0-beta02.txt
@@ -0,0 +1,33 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public abstract class LiveData<T> {
+ ctor public LiveData(T!);
+ ctor public LiveData();
+ method public T? getValue();
+ method public boolean hasActiveObservers();
+ method public boolean hasObservers();
+ method public boolean isInitialized();
+ method @MainThread public void observe(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Observer<? super T>);
+ method @MainThread public void observeForever(androidx.lifecycle.Observer<? super T>);
+ method protected void onActive();
+ method protected void onInactive();
+ method protected void postValue(T!);
+ method @MainThread public void removeObserver(androidx.lifecycle.Observer<? super T>);
+ method @MainThread public void removeObservers(androidx.lifecycle.LifecycleOwner);
+ method @MainThread protected void setValue(T!);
+ }
+
+ public class MutableLiveData<T> extends androidx.lifecycle.LiveData<T> {
+ ctor public MutableLiveData(T!);
+ ctor public MutableLiveData();
+ method public void postValue(T!);
+ method public void setValue(T!);
+ }
+
+ public fun interface Observer<T> {
+ method public void onChanged(T? value);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-livedata-core/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-livedata-core/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..f528b4e
--- /dev/null
+++ b/lifecycle/lifecycle-livedata-core/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,33 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public abstract class LiveData<T> {
+ ctor public LiveData(T!);
+ ctor public LiveData();
+ method public T? getValue();
+ method public boolean hasActiveObservers();
+ method public boolean hasObservers();
+ method public boolean isInitialized();
+ method @MainThread public void observe(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Observer<? super T>);
+ method @MainThread public void observeForever(androidx.lifecycle.Observer<? super T>);
+ method protected void onActive();
+ method protected void onInactive();
+ method protected void postValue(T!);
+ method @MainThread public void removeObserver(androidx.lifecycle.Observer<? super T>);
+ method @MainThread public void removeObservers(androidx.lifecycle.LifecycleOwner);
+ method @MainThread protected void setValue(T!);
+ }
+
+ public class MutableLiveData<T> extends androidx.lifecycle.LiveData<T> {
+ ctor public MutableLiveData(T!);
+ ctor public MutableLiveData();
+ method public void postValue(T!);
+ method public void setValue(T!);
+ }
+
+ public fun interface Observer<T> {
+ method public void onChanged(T? value);
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-livedata-core/api/res-2.6.0-beta02.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to lifecycle/lifecycle-livedata-core/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-livedata-core/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-livedata-core/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..f528b4e
--- /dev/null
+++ b/lifecycle/lifecycle-livedata-core/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,33 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public abstract class LiveData<T> {
+ ctor public LiveData(T!);
+ ctor public LiveData();
+ method public T? getValue();
+ method public boolean hasActiveObservers();
+ method public boolean hasObservers();
+ method public boolean isInitialized();
+ method @MainThread public void observe(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Observer<? super T>);
+ method @MainThread public void observeForever(androidx.lifecycle.Observer<? super T>);
+ method protected void onActive();
+ method protected void onInactive();
+ method protected void postValue(T!);
+ method @MainThread public void removeObserver(androidx.lifecycle.Observer<? super T>);
+ method @MainThread public void removeObservers(androidx.lifecycle.LifecycleOwner);
+ method @MainThread protected void setValue(T!);
+ }
+
+ public class MutableLiveData<T> extends androidx.lifecycle.LiveData<T> {
+ ctor public MutableLiveData(T!);
+ ctor public MutableLiveData();
+ method public void postValue(T!);
+ method public void setValue(T!);
+ }
+
+ public fun interface Observer<T> {
+ method public void onChanged(T? value);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-livedata-core/build.gradle b/lifecycle/lifecycle-livedata-core/build.gradle
index 1947720d..58101b7 100644
--- a/lifecycle/lifecycle-livedata-core/build.gradle
+++ b/lifecycle/lifecycle-livedata-core/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "Android Lifecycle LiveData Core"
+ name = "Lifecycle LiveData Core"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Lifecycle LiveData Core"
diff --git a/lifecycle/lifecycle-livedata-ktx/api/2.6.0-beta02.txt b/lifecycle/lifecycle-livedata-ktx/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..bae0928
--- /dev/null
+++ b/lifecycle/lifecycle-livedata-ktx/api/2.6.0-beta02.txt
@@ -0,0 +1,25 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class CoroutineLiveDataKt {
+ method public static <T> androidx.lifecycle.LiveData<T> liveData(optional kotlin.coroutines.CoroutineContext context, optional long timeoutInMs, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public static <T> androidx.lifecycle.LiveData<T> liveData(optional kotlin.coroutines.CoroutineContext context, java.time.Duration timeout, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ }
+
+ public final class FlowLiveDataConversions {
+ method public static <T> kotlinx.coroutines.flow.Flow<T> asFlow(androidx.lifecycle.LiveData<T>);
+ method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context, optional long timeoutInMs);
+ method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
+ method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>);
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context, java.time.Duration timeout);
+ }
+
+ public interface LiveDataScope<T> {
+ method public suspend Object? emit(T? value, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? emitSource(androidx.lifecycle.LiveData<T> source, kotlin.coroutines.Continuation<? super kotlinx.coroutines.DisposableHandle>);
+ method public T? getLatestValue();
+ property public abstract T? latestValue;
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-livedata-ktx/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-livedata-ktx/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..bae0928
--- /dev/null
+++ b/lifecycle/lifecycle-livedata-ktx/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,25 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class CoroutineLiveDataKt {
+ method public static <T> androidx.lifecycle.LiveData<T> liveData(optional kotlin.coroutines.CoroutineContext context, optional long timeoutInMs, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public static <T> androidx.lifecycle.LiveData<T> liveData(optional kotlin.coroutines.CoroutineContext context, java.time.Duration timeout, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ }
+
+ public final class FlowLiveDataConversions {
+ method public static <T> kotlinx.coroutines.flow.Flow<T> asFlow(androidx.lifecycle.LiveData<T>);
+ method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context, optional long timeoutInMs);
+ method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
+ method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>);
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context, java.time.Duration timeout);
+ }
+
+ public interface LiveDataScope<T> {
+ method public suspend Object? emit(T? value, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? emitSource(androidx.lifecycle.LiveData<T> source, kotlin.coroutines.Continuation<? super kotlinx.coroutines.DisposableHandle>);
+ method public T? getLatestValue();
+ property public abstract T? latestValue;
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-livedata-ktx/api/res-2.6.0-beta02.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to lifecycle/lifecycle-livedata-ktx/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-livedata-ktx/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-livedata-ktx/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..bae0928
--- /dev/null
+++ b/lifecycle/lifecycle-livedata-ktx/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,25 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class CoroutineLiveDataKt {
+ method public static <T> androidx.lifecycle.LiveData<T> liveData(optional kotlin.coroutines.CoroutineContext context, optional long timeoutInMs, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public static <T> androidx.lifecycle.LiveData<T> liveData(optional kotlin.coroutines.CoroutineContext context, java.time.Duration timeout, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ }
+
+ public final class FlowLiveDataConversions {
+ method public static <T> kotlinx.coroutines.flow.Flow<T> asFlow(androidx.lifecycle.LiveData<T>);
+ method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context, optional long timeoutInMs);
+ method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
+ method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>);
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context, java.time.Duration timeout);
+ }
+
+ public interface LiveDataScope<T> {
+ method public suspend Object? emit(T? value, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? emitSource(androidx.lifecycle.LiveData<T> source, kotlin.coroutines.Continuation<? super kotlinx.coroutines.DisposableHandle>);
+ method public T? getLatestValue();
+ property public abstract T? latestValue;
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-livedata/api/2.6.0-beta02.txt b/lifecycle/lifecycle-livedata/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..9b1bf6c
--- /dev/null
+++ b/lifecycle/lifecycle-livedata/api/2.6.0-beta02.txt
@@ -0,0 +1,20 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public class MediatorLiveData<T> extends androidx.lifecycle.MutableLiveData<T> {
+ ctor public MediatorLiveData();
+ ctor public MediatorLiveData(T!);
+ method @MainThread public <S> void addSource(androidx.lifecycle.LiveData<S!>, androidx.lifecycle.Observer<? super S>);
+ method @MainThread public <S> void removeSource(androidx.lifecycle.LiveData<S!>);
+ }
+
+ public final class Transformations {
+ method @CheckResult @MainThread public static <X> androidx.lifecycle.LiveData<X> distinctUntilChanged(androidx.lifecycle.LiveData<X>);
+ method @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> map(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<X,Y> transform);
+ method @Deprecated @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> map(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X,Y> mapFunction);
+ method @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> switchMap(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<X,androidx.lifecycle.LiveData<Y>> transform);
+ method @Deprecated @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> switchMap(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X,androidx.lifecycle.LiveData<Y>> switchMapFunction);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-livedata/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-livedata/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..9b1bf6c
--- /dev/null
+++ b/lifecycle/lifecycle-livedata/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,20 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public class MediatorLiveData<T> extends androidx.lifecycle.MutableLiveData<T> {
+ ctor public MediatorLiveData();
+ ctor public MediatorLiveData(T!);
+ method @MainThread public <S> void addSource(androidx.lifecycle.LiveData<S!>, androidx.lifecycle.Observer<? super S>);
+ method @MainThread public <S> void removeSource(androidx.lifecycle.LiveData<S!>);
+ }
+
+ public final class Transformations {
+ method @CheckResult @MainThread public static <X> androidx.lifecycle.LiveData<X> distinctUntilChanged(androidx.lifecycle.LiveData<X>);
+ method @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> map(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<X,Y> transform);
+ method @Deprecated @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> map(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X,Y> mapFunction);
+ method @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> switchMap(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<X,androidx.lifecycle.LiveData<Y>> transform);
+ method @Deprecated @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> switchMap(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X,androidx.lifecycle.LiveData<Y>> switchMapFunction);
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-livedata/api/res-2.6.0-beta02.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to lifecycle/lifecycle-livedata/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-livedata/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-livedata/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..bb61b39
--- /dev/null
+++ b/lifecycle/lifecycle-livedata/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,29 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class ComputableLiveData<T> {
+ ctor public ComputableLiveData(optional java.util.concurrent.Executor executor);
+ ctor public ComputableLiveData();
+ method @WorkerThread protected abstract T! compute();
+ method public androidx.lifecycle.LiveData<T> getLiveData();
+ method public void invalidate();
+ property public androidx.lifecycle.LiveData<T> liveData;
+ }
+
+ public class MediatorLiveData<T> extends androidx.lifecycle.MutableLiveData<T> {
+ ctor public MediatorLiveData();
+ ctor public MediatorLiveData(T!);
+ method @MainThread public <S> void addSource(androidx.lifecycle.LiveData<S!>, androidx.lifecycle.Observer<? super S>);
+ method @MainThread public <S> void removeSource(androidx.lifecycle.LiveData<S!>);
+ }
+
+ public final class Transformations {
+ method @CheckResult @MainThread public static <X> androidx.lifecycle.LiveData<X> distinctUntilChanged(androidx.lifecycle.LiveData<X>);
+ method @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> map(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<X,Y> transform);
+ method @Deprecated @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> map(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X,Y> mapFunction);
+ method @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> switchMap(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<X,androidx.lifecycle.LiveData<Y>> transform);
+ method @Deprecated @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> switchMap(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X,androidx.lifecycle.LiveData<Y>> switchMapFunction);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-livedata/build.gradle b/lifecycle/lifecycle-livedata/build.gradle
index 824c7f0..898e7b1 100644
--- a/lifecycle/lifecycle-livedata/build.gradle
+++ b/lifecycle/lifecycle-livedata/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "Android Lifecycle LiveData"
+ name = "Lifecycle LiveData"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Lifecycle LiveData"
diff --git a/lifecycle/lifecycle-process/api/2.6.0-beta02.txt b/lifecycle/lifecycle-process/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..891c9c6
--- /dev/null
+++ b/lifecycle/lifecycle-process/api/2.6.0-beta02.txt
@@ -0,0 +1,22 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class ProcessLifecycleInitializer implements androidx.startup.Initializer<androidx.lifecycle.LifecycleOwner> {
+ ctor public ProcessLifecycleInitializer();
+ method public androidx.lifecycle.LifecycleOwner create(android.content.Context context);
+ method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>> dependencies();
+ }
+
+ public final class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
+ method public static androidx.lifecycle.LifecycleOwner get();
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ property public androidx.lifecycle.Lifecycle lifecycle;
+ field public static final androidx.lifecycle.ProcessLifecycleOwner.Companion Companion;
+ }
+
+ public static final class ProcessLifecycleOwner.Companion {
+ method public androidx.lifecycle.LifecycleOwner get();
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-process/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-process/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..891c9c6
--- /dev/null
+++ b/lifecycle/lifecycle-process/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,22 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class ProcessLifecycleInitializer implements androidx.startup.Initializer<androidx.lifecycle.LifecycleOwner> {
+ ctor public ProcessLifecycleInitializer();
+ method public androidx.lifecycle.LifecycleOwner create(android.content.Context context);
+ method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>> dependencies();
+ }
+
+ public final class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
+ method public static androidx.lifecycle.LifecycleOwner get();
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ property public androidx.lifecycle.Lifecycle lifecycle;
+ field public static final androidx.lifecycle.ProcessLifecycleOwner.Companion Companion;
+ }
+
+ public static final class ProcessLifecycleOwner.Companion {
+ method public androidx.lifecycle.LifecycleOwner get();
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-process/api/res-2.6.0-beta02.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to lifecycle/lifecycle-process/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-process/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-process/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..891c9c6
--- /dev/null
+++ b/lifecycle/lifecycle-process/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,22 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class ProcessLifecycleInitializer implements androidx.startup.Initializer<androidx.lifecycle.LifecycleOwner> {
+ ctor public ProcessLifecycleInitializer();
+ method public androidx.lifecycle.LifecycleOwner create(android.content.Context context);
+ method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>> dependencies();
+ }
+
+ public final class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
+ method public static androidx.lifecycle.LifecycleOwner get();
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ property public androidx.lifecycle.Lifecycle lifecycle;
+ field public static final androidx.lifecycle.ProcessLifecycleOwner.Companion Companion;
+ }
+
+ public static final class ProcessLifecycleOwner.Companion {
+ method public androidx.lifecycle.LifecycleOwner get();
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-process/build.gradle b/lifecycle/lifecycle-process/build.gradle
index 09a4076..2c59ba1f5 100644
--- a/lifecycle/lifecycle-process/build.gradle
+++ b/lifecycle/lifecycle-process/build.gradle
@@ -41,7 +41,7 @@
}
androidx {
- name = "Android Lifecycle Process"
+ name = "Lifecycle Process"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Android Lifecycle Process"
diff --git a/lifecycle/lifecycle-reactivestreams-ktx/api/2.6.0-beta02.txt b/lifecycle/lifecycle-reactivestreams-ktx/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/lifecycle/lifecycle-reactivestreams-ktx/api/2.6.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/lifecycle/lifecycle-reactivestreams-ktx/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-reactivestreams-ktx/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/lifecycle/lifecycle-reactivestreams-ktx/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-reactivestreams-ktx/api/res-2.6.0-beta02.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to lifecycle/lifecycle-reactivestreams-ktx/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-reactivestreams-ktx/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-reactivestreams-ktx/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/lifecycle/lifecycle-reactivestreams-ktx/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/lifecycle/lifecycle-reactivestreams-ktx/build.gradle b/lifecycle/lifecycle-reactivestreams-ktx/build.gradle
index 79856f6..2fef830 100644
--- a/lifecycle/lifecycle-reactivestreams-ktx/build.gradle
+++ b/lifecycle/lifecycle-reactivestreams-ktx/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "Android Lifecycle ReactiveStreams KTX"
+ name = "Lifecycle ReactiveStreams KTX"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Kotlin extensions for Lifecycle ReactiveStreams"
diff --git a/lifecycle/lifecycle-reactivestreams/api/2.6.0-beta02.txt b/lifecycle/lifecycle-reactivestreams/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..138dd3e
--- /dev/null
+++ b/lifecycle/lifecycle-reactivestreams/api/2.6.0-beta02.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class LiveDataReactiveStreams {
+ method public static <T> androidx.lifecycle.LiveData<T> fromPublisher(org.reactivestreams.Publisher<T>);
+ method public static <T> org.reactivestreams.Publisher<T> toPublisher(androidx.lifecycle.LifecycleOwner lifecycle, androidx.lifecycle.LiveData<T> liveData);
+ method public static <T> org.reactivestreams.Publisher<T> toPublisher(androidx.lifecycle.LiveData<T>, androidx.lifecycle.LifecycleOwner lifecycle);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-reactivestreams/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-reactivestreams/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..138dd3e
--- /dev/null
+++ b/lifecycle/lifecycle-reactivestreams/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class LiveDataReactiveStreams {
+ method public static <T> androidx.lifecycle.LiveData<T> fromPublisher(org.reactivestreams.Publisher<T>);
+ method public static <T> org.reactivestreams.Publisher<T> toPublisher(androidx.lifecycle.LifecycleOwner lifecycle, androidx.lifecycle.LiveData<T> liveData);
+ method public static <T> org.reactivestreams.Publisher<T> toPublisher(androidx.lifecycle.LiveData<T>, androidx.lifecycle.LifecycleOwner lifecycle);
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-reactivestreams/api/res-2.6.0-beta02.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to lifecycle/lifecycle-reactivestreams/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-reactivestreams/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-reactivestreams/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..138dd3e
--- /dev/null
+++ b/lifecycle/lifecycle-reactivestreams/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class LiveDataReactiveStreams {
+ method public static <T> androidx.lifecycle.LiveData<T> fromPublisher(org.reactivestreams.Publisher<T>);
+ method public static <T> org.reactivestreams.Publisher<T> toPublisher(androidx.lifecycle.LifecycleOwner lifecycle, androidx.lifecycle.LiveData<T> liveData);
+ method public static <T> org.reactivestreams.Publisher<T> toPublisher(androidx.lifecycle.LiveData<T>, androidx.lifecycle.LifecycleOwner lifecycle);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-reactivestreams/build.gradle b/lifecycle/lifecycle-reactivestreams/build.gradle
index e47bb63..e73e520b 100644
--- a/lifecycle/lifecycle-reactivestreams/build.gradle
+++ b/lifecycle/lifecycle-reactivestreams/build.gradle
@@ -43,7 +43,7 @@
}
androidx {
- name = "Android Lifecycle Reactivestreams"
+ name = "Lifecycle Reactivestreams"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Lifecycle Reactivestreams"
diff --git a/lifecycle/lifecycle-runtime-compose/api/2.6.0-beta02.txt b/lifecycle/lifecycle-runtime-compose/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..c80fa83
--- /dev/null
+++ b/lifecycle/lifecycle-runtime-compose/api/2.6.0-beta02.txt
@@ -0,0 +1,12 @@
+// Signature format: 4.0
+package androidx.lifecycle.compose {
+
+ public final class FlowExtKt {
+ method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsStateWithLifecycle(kotlinx.coroutines.flow.StateFlow<? extends T>, optional androidx.lifecycle.LifecycleOwner lifecycleOwner, optional androidx.lifecycle.Lifecycle.State minActiveState, optional kotlin.coroutines.CoroutineContext context);
+ method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsStateWithLifecycle(kotlinx.coroutines.flow.StateFlow<? extends T>, androidx.lifecycle.Lifecycle lifecycle, optional androidx.lifecycle.Lifecycle.State minActiveState, optional kotlin.coroutines.CoroutineContext context);
+ method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsStateWithLifecycle(kotlinx.coroutines.flow.Flow<? extends T>, T? initialValue, optional androidx.lifecycle.LifecycleOwner lifecycleOwner, optional androidx.lifecycle.Lifecycle.State minActiveState, optional kotlin.coroutines.CoroutineContext context);
+ method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsStateWithLifecycle(kotlinx.coroutines.flow.Flow<? extends T>, T? initialValue, androidx.lifecycle.Lifecycle lifecycle, optional androidx.lifecycle.Lifecycle.State minActiveState, optional kotlin.coroutines.CoroutineContext context);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..c80fa83
--- /dev/null
+++ b/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,12 @@
+// Signature format: 4.0
+package androidx.lifecycle.compose {
+
+ public final class FlowExtKt {
+ method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsStateWithLifecycle(kotlinx.coroutines.flow.StateFlow<? extends T>, optional androidx.lifecycle.LifecycleOwner lifecycleOwner, optional androidx.lifecycle.Lifecycle.State minActiveState, optional kotlin.coroutines.CoroutineContext context);
+ method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsStateWithLifecycle(kotlinx.coroutines.flow.StateFlow<? extends T>, androidx.lifecycle.Lifecycle lifecycle, optional androidx.lifecycle.Lifecycle.State minActiveState, optional kotlin.coroutines.CoroutineContext context);
+ method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsStateWithLifecycle(kotlinx.coroutines.flow.Flow<? extends T>, T? initialValue, optional androidx.lifecycle.LifecycleOwner lifecycleOwner, optional androidx.lifecycle.Lifecycle.State minActiveState, optional kotlin.coroutines.CoroutineContext context);
+ method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsStateWithLifecycle(kotlinx.coroutines.flow.Flow<? extends T>, T? initialValue, androidx.lifecycle.Lifecycle lifecycle, optional androidx.lifecycle.Lifecycle.State minActiveState, optional kotlin.coroutines.CoroutineContext context);
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-runtime-compose/api/res-2.6.0-beta02.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to lifecycle/lifecycle-runtime-compose/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-runtime-compose/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-runtime-compose/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..c80fa83
--- /dev/null
+++ b/lifecycle/lifecycle-runtime-compose/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,12 @@
+// Signature format: 4.0
+package androidx.lifecycle.compose {
+
+ public final class FlowExtKt {
+ method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsStateWithLifecycle(kotlinx.coroutines.flow.StateFlow<? extends T>, optional androidx.lifecycle.LifecycleOwner lifecycleOwner, optional androidx.lifecycle.Lifecycle.State minActiveState, optional kotlin.coroutines.CoroutineContext context);
+ method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsStateWithLifecycle(kotlinx.coroutines.flow.StateFlow<? extends T>, androidx.lifecycle.Lifecycle lifecycle, optional androidx.lifecycle.Lifecycle.State minActiveState, optional kotlin.coroutines.CoroutineContext context);
+ method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsStateWithLifecycle(kotlinx.coroutines.flow.Flow<? extends T>, T? initialValue, optional androidx.lifecycle.LifecycleOwner lifecycleOwner, optional androidx.lifecycle.Lifecycle.State minActiveState, optional kotlin.coroutines.CoroutineContext context);
+ method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsStateWithLifecycle(kotlinx.coroutines.flow.Flow<? extends T>, T? initialValue, androidx.lifecycle.Lifecycle lifecycle, optional androidx.lifecycle.Lifecycle.State minActiveState, optional kotlin.coroutines.CoroutineContext context);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-runtime-compose/samples/build.gradle b/lifecycle/lifecycle-runtime-compose/samples/build.gradle
index b1f8730..334768e 100644
--- a/lifecycle/lifecycle-runtime-compose/samples/build.gradle
+++ b/lifecycle/lifecycle-runtime-compose/samples/build.gradle
@@ -33,7 +33,7 @@
}
androidx {
- name = "AndroidX Lifecycle Runtime Compose Integration Samples"
+ name = "Lifecycle Runtime Compose Integration Samples"
type = LibraryType.SAMPLES
inceptionYear = "2021"
description = "Samples for Compose integration with Lifecycle Runtime"
diff --git a/lifecycle/lifecycle-runtime-ktx-lint/build.gradle b/lifecycle/lifecycle-runtime-ktx-lint/build.gradle
index 3761f26..e10aea4 100644
--- a/lifecycle/lifecycle-runtime-ktx-lint/build.gradle
+++ b/lifecycle/lifecycle-runtime-ktx-lint/build.gradle
@@ -35,7 +35,7 @@
}
androidx {
- name = "Android Lifecycles Lint Checks"
+ name = "Lifecycles Lint Checks"
type = LibraryType.LINT
inceptionYear = "2019"
description = "Android Lifecycles Lint Checks"
diff --git a/lifecycle/lifecycle-runtime-ktx/api/2.6.0-beta02.txt b/lifecycle/lifecycle-runtime-ktx/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..2ee0d85
--- /dev/null
+++ b/lifecycle/lifecycle-runtime-ktx/api/2.6.0-beta02.txt
@@ -0,0 +1,33 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class FlowExtKt {
+ method public static <T> kotlinx.coroutines.flow.Flow<T> flowWithLifecycle(kotlinx.coroutines.flow.Flow<? extends T>, androidx.lifecycle.Lifecycle lifecycle, optional androidx.lifecycle.Lifecycle.State minActiveState);
+ }
+
+ public final class LifecycleDestroyedException extends java.util.concurrent.CancellationException {
+ ctor public LifecycleDestroyedException();
+ }
+
+ public final class RepeatOnLifecycleKt {
+ method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ }
+
+ public final class ViewKt {
+ method @Deprecated public static androidx.lifecycle.LifecycleOwner? findViewTreeLifecycleOwner(android.view.View);
+ }
+
+ public final class WithLifecycleStateKt {
+ method public static suspend inline <R> Object? withCreated(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withCreated(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withResumed(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withResumed(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withStarted(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withStarted(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withStateAtLeast(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withStateAtLeast(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-runtime-ktx/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-runtime-ktx/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..2ee0d85
--- /dev/null
+++ b/lifecycle/lifecycle-runtime-ktx/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,33 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class FlowExtKt {
+ method public static <T> kotlinx.coroutines.flow.Flow<T> flowWithLifecycle(kotlinx.coroutines.flow.Flow<? extends T>, androidx.lifecycle.Lifecycle lifecycle, optional androidx.lifecycle.Lifecycle.State minActiveState);
+ }
+
+ public final class LifecycleDestroyedException extends java.util.concurrent.CancellationException {
+ ctor public LifecycleDestroyedException();
+ }
+
+ public final class RepeatOnLifecycleKt {
+ method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ }
+
+ public final class ViewKt {
+ method @Deprecated public static androidx.lifecycle.LifecycleOwner? findViewTreeLifecycleOwner(android.view.View);
+ }
+
+ public final class WithLifecycleStateKt {
+ method public static suspend inline <R> Object? withCreated(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withCreated(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withResumed(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withResumed(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withStarted(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withStarted(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withStateAtLeast(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withStateAtLeast(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-runtime-ktx/api/res-2.6.0-beta02.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to lifecycle/lifecycle-runtime-ktx/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-runtime-ktx/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-runtime-ktx/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..a998f6e
--- /dev/null
+++ b/lifecycle/lifecycle-runtime-ktx/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class FlowExtKt {
+ method public static <T> kotlinx.coroutines.flow.Flow<T> flowWithLifecycle(kotlinx.coroutines.flow.Flow<? extends T>, androidx.lifecycle.Lifecycle lifecycle, optional androidx.lifecycle.Lifecycle.State minActiveState);
+ }
+
+ public final class LifecycleDestroyedException extends java.util.concurrent.CancellationException {
+ ctor public LifecycleDestroyedException();
+ }
+
+ public final class RepeatOnLifecycleKt {
+ method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ }
+
+ public final class ViewKt {
+ method @Deprecated public static androidx.lifecycle.LifecycleOwner? findViewTreeLifecycleOwner(android.view.View);
+ }
+
+ public final class WithLifecycleStateKt {
+ method @kotlin.PublishedApi internal static suspend <R> Object? suspendWithStateAtLeastUnchecked(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, boolean dispatchNeeded, kotlinx.coroutines.CoroutineDispatcher lifecycleDispatcher, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withCreated(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withCreated(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withResumed(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withResumed(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withStarted(androidx.lifecycle.Lifecycle, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withStarted(androidx.lifecycle.LifecycleOwner, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withStateAtLeast(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend inline <R> Object? withStateAtLeast(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ method @kotlin.PublishedApi internal static suspend inline <R> Object? withStateAtLeastUnchecked(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function0<? extends R> block, kotlin.coroutines.Continuation<? super R>);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-runtime-ktx/build.gradle b/lifecycle/lifecycle-runtime-ktx/build.gradle
index e394b0b..554d287 100644
--- a/lifecycle/lifecycle-runtime-ktx/build.gradle
+++ b/lifecycle/lifecycle-runtime-ktx/build.gradle
@@ -44,7 +44,7 @@
}
androidx {
- name = "Android Lifecycle Kotlin Extensions"
+ name = "Lifecycle Kotlin Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
description = "Kotlin extensions for 'lifecycle' artifact"
diff --git a/lifecycle/lifecycle-runtime-testing/api/2.6.0-beta02.txt b/lifecycle/lifecycle-runtime-testing/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..47a819e
--- /dev/null
+++ b/lifecycle/lifecycle-runtime-testing/api/2.6.0-beta02.txt
@@ -0,0 +1,19 @@
+// Signature format: 4.0
+package androidx.lifecycle.testing {
+
+ public final class TestLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
+ ctor public TestLifecycleOwner(optional androidx.lifecycle.Lifecycle.State initialState, optional kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher);
+ ctor public TestLifecycleOwner(optional androidx.lifecycle.Lifecycle.State initialState);
+ ctor public TestLifecycleOwner();
+ method public androidx.lifecycle.Lifecycle.State getCurrentState();
+ method public androidx.lifecycle.LifecycleRegistry getLifecycle();
+ method public int getObserverCount();
+ method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
+ method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+ property public final androidx.lifecycle.Lifecycle.State currentState;
+ property public androidx.lifecycle.LifecycleRegistry lifecycle;
+ property public final int observerCount;
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-runtime-testing/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-runtime-testing/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..47a819e
--- /dev/null
+++ b/lifecycle/lifecycle-runtime-testing/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,19 @@
+// Signature format: 4.0
+package androidx.lifecycle.testing {
+
+ public final class TestLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
+ ctor public TestLifecycleOwner(optional androidx.lifecycle.Lifecycle.State initialState, optional kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher);
+ ctor public TestLifecycleOwner(optional androidx.lifecycle.Lifecycle.State initialState);
+ ctor public TestLifecycleOwner();
+ method public androidx.lifecycle.Lifecycle.State getCurrentState();
+ method public androidx.lifecycle.LifecycleRegistry getLifecycle();
+ method public int getObserverCount();
+ method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
+ method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+ property public final androidx.lifecycle.Lifecycle.State currentState;
+ property public androidx.lifecycle.LifecycleRegistry lifecycle;
+ property public final int observerCount;
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-runtime-testing/api/res-2.6.0-beta02.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to lifecycle/lifecycle-runtime-testing/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-runtime-testing/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-runtime-testing/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..47a819e
--- /dev/null
+++ b/lifecycle/lifecycle-runtime-testing/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,19 @@
+// Signature format: 4.0
+package androidx.lifecycle.testing {
+
+ public final class TestLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
+ ctor public TestLifecycleOwner(optional androidx.lifecycle.Lifecycle.State initialState, optional kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher);
+ ctor public TestLifecycleOwner(optional androidx.lifecycle.Lifecycle.State initialState);
+ ctor public TestLifecycleOwner();
+ method public androidx.lifecycle.Lifecycle.State getCurrentState();
+ method public androidx.lifecycle.LifecycleRegistry getLifecycle();
+ method public int getObserverCount();
+ method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
+ method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+ property public final androidx.lifecycle.Lifecycle.State currentState;
+ property public androidx.lifecycle.LifecycleRegistry lifecycle;
+ property public final int observerCount;
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-runtime-testing/build.gradle b/lifecycle/lifecycle-runtime-testing/build.gradle
index 4e59db92..3c7aca3 100644
--- a/lifecycle/lifecycle-runtime-testing/build.gradle
+++ b/lifecycle/lifecycle-runtime-testing/build.gradle
@@ -39,7 +39,7 @@
}
androidx {
- name = "Android Lifecycle Runtime Testing"
+ name = "Lifecycle Runtime Testing"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
description = "Testing utilities for 'lifecycle' artifact"
diff --git a/lifecycle/lifecycle-runtime/api/2.6.0-beta02.txt b/lifecycle/lifecycle-runtime/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..e72bd60
--- /dev/null
+++ b/lifecycle/lifecycle-runtime/api/2.6.0-beta02.txt
@@ -0,0 +1,33 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public class LifecycleRegistry extends androidx.lifecycle.Lifecycle {
+ ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner provider);
+ method public void addObserver(androidx.lifecycle.LifecycleObserver observer);
+ method @VisibleForTesting public static final androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
+ method public androidx.lifecycle.Lifecycle.State getCurrentState();
+ method public int getObserverCount();
+ method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
+ method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State state);
+ method public void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+ method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+ property public androidx.lifecycle.Lifecycle.State currentState;
+ property public int observerCount;
+ field public static final androidx.lifecycle.LifecycleRegistry.Companion Companion;
+ }
+
+ public static final class LifecycleRegistry.Companion {
+ method @VisibleForTesting public androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
+ }
+
+ @Deprecated public interface LifecycleRegistryOwner extends androidx.lifecycle.LifecycleOwner {
+ method @Deprecated public androidx.lifecycle.LifecycleRegistry getLifecycle();
+ }
+
+ public final class ViewTreeLifecycleOwner {
+ method public static androidx.lifecycle.LifecycleOwner? get(android.view.View);
+ method public static void set(android.view.View, androidx.lifecycle.LifecycleOwner? lifecycleOwner);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-runtime/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-runtime/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..e72bd60
--- /dev/null
+++ b/lifecycle/lifecycle-runtime/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,33 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public class LifecycleRegistry extends androidx.lifecycle.Lifecycle {
+ ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner provider);
+ method public void addObserver(androidx.lifecycle.LifecycleObserver observer);
+ method @VisibleForTesting public static final androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
+ method public androidx.lifecycle.Lifecycle.State getCurrentState();
+ method public int getObserverCount();
+ method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
+ method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State state);
+ method public void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+ method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+ property public androidx.lifecycle.Lifecycle.State currentState;
+ property public int observerCount;
+ field public static final androidx.lifecycle.LifecycleRegistry.Companion Companion;
+ }
+
+ public static final class LifecycleRegistry.Companion {
+ method @VisibleForTesting public androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
+ }
+
+ @Deprecated public interface LifecycleRegistryOwner extends androidx.lifecycle.LifecycleOwner {
+ method @Deprecated public androidx.lifecycle.LifecycleRegistry getLifecycle();
+ }
+
+ public final class ViewTreeLifecycleOwner {
+ method public static androidx.lifecycle.LifecycleOwner? get(android.view.View);
+ method public static void set(android.view.View, androidx.lifecycle.LifecycleOwner? lifecycleOwner);
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-runtime/api/res-2.6.0-beta02.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to lifecycle/lifecycle-runtime/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-runtime/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-runtime/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..704cdb4
--- /dev/null
+++ b/lifecycle/lifecycle-runtime/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,58 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public class LifecycleRegistry extends androidx.lifecycle.Lifecycle {
+ ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner provider);
+ method public void addObserver(androidx.lifecycle.LifecycleObserver observer);
+ method @VisibleForTesting public static final androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
+ method public androidx.lifecycle.Lifecycle.State getCurrentState();
+ method public int getObserverCount();
+ method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
+ method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State state);
+ method public void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+ method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+ property public androidx.lifecycle.Lifecycle.State currentState;
+ property public int observerCount;
+ field public static final androidx.lifecycle.LifecycleRegistry.Companion Companion;
+ }
+
+ public static final class LifecycleRegistry.Companion {
+ method @VisibleForTesting public androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
+ }
+
+ @Deprecated public interface LifecycleRegistryOwner extends androidx.lifecycle.LifecycleOwner {
+ method @Deprecated public androidx.lifecycle.LifecycleRegistry getLifecycle();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class ReportFragment extends android.app.Fragment {
+ ctor public ReportFragment();
+ method public static final androidx.lifecycle.ReportFragment get(android.app.Activity);
+ method public static final void injectIfNeededIn(android.app.Activity activity);
+ method public void onActivityCreated(android.os.Bundle? savedInstanceState);
+ method public void onDestroy();
+ method public void onPause();
+ method public void onResume();
+ method public void onStart();
+ method public void onStop();
+ method public final void setProcessListener(androidx.lifecycle.ReportFragment.ActivityInitializationListener? processListener);
+ field public static final androidx.lifecycle.ReportFragment.Companion Companion;
+ }
+
+ public static interface ReportFragment.ActivityInitializationListener {
+ method public void onCreate();
+ method public void onResume();
+ method public void onStart();
+ }
+
+ public static final class ReportFragment.Companion {
+ method public androidx.lifecycle.ReportFragment get(android.app.Activity);
+ method public void injectIfNeededIn(android.app.Activity activity);
+ }
+
+ public final class ViewTreeLifecycleOwner {
+ method public static androidx.lifecycle.LifecycleOwner? get(android.view.View);
+ method public static void set(android.view.View, androidx.lifecycle.LifecycleOwner? lifecycleOwner);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-runtime/build.gradle b/lifecycle/lifecycle-runtime/build.gradle
index 5199df9..3e06c78 100644
--- a/lifecycle/lifecycle-runtime/build.gradle
+++ b/lifecycle/lifecycle-runtime/build.gradle
@@ -33,7 +33,7 @@
}
androidx {
- name "Android Lifecycle Runtime"
+ name = "Lifecycle Runtime"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear "2017"
description "Android Lifecycle Runtime"
diff --git a/lifecycle/lifecycle-service/api/2.6.0-beta02.txt b/lifecycle/lifecycle-service/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..bebcd93
--- /dev/null
+++ b/lifecycle/lifecycle-service/api/2.6.0-beta02.txt
@@ -0,0 +1,22 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public class LifecycleService extends android.app.Service implements androidx.lifecycle.LifecycleOwner {
+ ctor public LifecycleService();
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ method @CallSuper public android.os.IBinder? onBind(android.content.Intent intent);
+ property public androidx.lifecycle.Lifecycle lifecycle;
+ }
+
+ public class ServiceLifecycleDispatcher {
+ ctor public ServiceLifecycleDispatcher(androidx.lifecycle.LifecycleOwner provider);
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ method public void onServicePreSuperOnBind();
+ method public void onServicePreSuperOnCreate();
+ method public void onServicePreSuperOnDestroy();
+ method public void onServicePreSuperOnStart();
+ property public androidx.lifecycle.Lifecycle lifecycle;
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-service/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-service/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..bebcd93
--- /dev/null
+++ b/lifecycle/lifecycle-service/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,22 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public class LifecycleService extends android.app.Service implements androidx.lifecycle.LifecycleOwner {
+ ctor public LifecycleService();
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ method @CallSuper public android.os.IBinder? onBind(android.content.Intent intent);
+ property public androidx.lifecycle.Lifecycle lifecycle;
+ }
+
+ public class ServiceLifecycleDispatcher {
+ ctor public ServiceLifecycleDispatcher(androidx.lifecycle.LifecycleOwner provider);
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ method public void onServicePreSuperOnBind();
+ method public void onServicePreSuperOnCreate();
+ method public void onServicePreSuperOnDestroy();
+ method public void onServicePreSuperOnStart();
+ property public androidx.lifecycle.Lifecycle lifecycle;
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-service/api/res-2.6.0-beta02.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to lifecycle/lifecycle-service/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-service/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-service/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..bebcd93
--- /dev/null
+++ b/lifecycle/lifecycle-service/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,22 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public class LifecycleService extends android.app.Service implements androidx.lifecycle.LifecycleOwner {
+ ctor public LifecycleService();
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ method @CallSuper public android.os.IBinder? onBind(android.content.Intent intent);
+ property public androidx.lifecycle.Lifecycle lifecycle;
+ }
+
+ public class ServiceLifecycleDispatcher {
+ ctor public ServiceLifecycleDispatcher(androidx.lifecycle.LifecycleOwner provider);
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ method public void onServicePreSuperOnBind();
+ method public void onServicePreSuperOnCreate();
+ method public void onServicePreSuperOnDestroy();
+ method public void onServicePreSuperOnStart();
+ property public androidx.lifecycle.Lifecycle lifecycle;
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-service/build.gradle b/lifecycle/lifecycle-service/build.gradle
index 690553f..7ab4fc2 100644
--- a/lifecycle/lifecycle-service/build.gradle
+++ b/lifecycle/lifecycle-service/build.gradle
@@ -34,7 +34,7 @@
}
androidx {
- name = "Android Lifecycle Service"
+ name = "Lifecycle Service"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Android Lifecycle Service"
diff --git a/lifecycle/lifecycle-viewmodel-compose/api/2.6.0-beta02.txt b/lifecycle/lifecycle-viewmodel-compose/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..05b6910
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-compose/api/2.6.0-beta02.txt
@@ -0,0 +1,23 @@
+// Signature format: 4.0
+package androidx.lifecycle.viewmodel.compose {
+
+ public final class LocalViewModelStoreOwner {
+ method @androidx.compose.runtime.Composable public androidx.lifecycle.ViewModelStoreOwner? getCurrent();
+ method public infix androidx.compose.runtime.ProvidedValue<androidx.lifecycle.ViewModelStoreOwner> provides(androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner);
+ property @androidx.compose.runtime.Composable public final androidx.lifecycle.ViewModelStoreOwner? current;
+ field public static final androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner INSTANCE;
+ }
+
+ public final class SavedStateHandleSaverKt {
+ }
+
+ public final class ViewModelKt {
+ method @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
+ method @Deprecated @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
+ method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
+ method @Deprecated @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
+ method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends VM> initializer);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-viewmodel-compose/api/current.ignore b/lifecycle/lifecycle-viewmodel-compose/api/current.ignore
new file mode 100644
index 0000000..0a5b8ff
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-compose/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: androidx.lifecycle.viewmodel.compose.SavedStateHandleSaverKt:
+ Removed class androidx.lifecycle.viewmodel.compose.SavedStateHandleSaverKt
diff --git a/lifecycle/lifecycle-viewmodel-compose/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-viewmodel-compose/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..188b922
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-compose/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.lifecycle.viewmodel.compose {
+
+ public final class LocalViewModelStoreOwner {
+ method @androidx.compose.runtime.Composable public androidx.lifecycle.ViewModelStoreOwner? getCurrent();
+ method public infix androidx.compose.runtime.ProvidedValue<androidx.lifecycle.ViewModelStoreOwner> provides(androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner);
+ property @androidx.compose.runtime.Composable public final androidx.lifecycle.ViewModelStoreOwner? current;
+ field public static final androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner INSTANCE;
+ }
+
+ @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.WARNING) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.RUNTIME) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface SavedStateHandleSaveableApi {
+ }
+
+ public final class SavedStateHandleSaverKt {
+ method @androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi public static <T> T saveable(androidx.lifecycle.SavedStateHandle, String key, optional androidx.compose.runtime.saveable.Saver<T,?> saver, kotlin.jvm.functions.Function0<? extends T> init);
+ method @androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi public static <T> androidx.compose.runtime.MutableState<T> saveable(androidx.lifecycle.SavedStateHandle, String key, androidx.compose.runtime.saveable.Saver<T,?> stateSaver, kotlin.jvm.functions.Function0<? extends androidx.compose.runtime.MutableState<T>> init);
+ method @androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi public static <T> kotlin.properties.PropertyDelegateProvider<java.lang.Object,kotlin.properties.ReadOnlyProperty<java.lang.Object,T>> saveable(androidx.lifecycle.SavedStateHandle, optional androidx.compose.runtime.saveable.Saver<T,?> saver, kotlin.jvm.functions.Function0<? extends T> init);
+ method @androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi public static <T, M extends androidx.compose.runtime.MutableState<T>> kotlin.properties.PropertyDelegateProvider<java.lang.Object,kotlin.properties.ReadWriteProperty<java.lang.Object,T>> saveableMutableState(androidx.lifecycle.SavedStateHandle, optional androidx.compose.runtime.saveable.Saver<T,?> stateSaver, kotlin.jvm.functions.Function0<? extends M> init);
+ }
+
+ public final class ViewModelKt {
+ method @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
+ method @Deprecated @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
+ method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
+ method @Deprecated @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
+ method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends VM> initializer);
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-viewmodel-compose/api/res-2.6.0-beta02.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to lifecycle/lifecycle-viewmodel-compose/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-viewmodel-compose/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-viewmodel-compose/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..05b6910
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-compose/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,23 @@
+// Signature format: 4.0
+package androidx.lifecycle.viewmodel.compose {
+
+ public final class LocalViewModelStoreOwner {
+ method @androidx.compose.runtime.Composable public androidx.lifecycle.ViewModelStoreOwner? getCurrent();
+ method public infix androidx.compose.runtime.ProvidedValue<androidx.lifecycle.ViewModelStoreOwner> provides(androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner);
+ property @androidx.compose.runtime.Composable public final androidx.lifecycle.ViewModelStoreOwner? current;
+ field public static final androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner INSTANCE;
+ }
+
+ public final class SavedStateHandleSaverKt {
+ }
+
+ public final class ViewModelKt {
+ method @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
+ method @Deprecated @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
+ method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory, optional androidx.lifecycle.viewmodel.CreationExtras extras);
+ method @Deprecated @androidx.compose.runtime.Composable public static <VM extends androidx.lifecycle.ViewModel> VM viewModel(Class<VM> modelClass, optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, optional androidx.lifecycle.ViewModelProvider.Factory? factory);
+ method @androidx.compose.runtime.Composable public static inline <reified VM extends androidx.lifecycle.ViewModel> VM viewModel(optional androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, optional String? key, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends VM> initializer);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-viewmodel-compose/api/restricted_current.ignore b/lifecycle/lifecycle-viewmodel-compose/api/restricted_current.ignore
new file mode 100644
index 0000000..0a5b8ff
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-compose/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: androidx.lifecycle.viewmodel.compose.SavedStateHandleSaverKt:
+ Removed class androidx.lifecycle.viewmodel.compose.SavedStateHandleSaverKt
diff --git a/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle b/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle
index 195fb57..f812410 100644
--- a/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle
@@ -33,7 +33,7 @@
}
androidx {
- name = "AndroidX Lifecycle ViewModel Compose Integration Samples"
+ name = "Lifecycle ViewModel Compose Integration Samples"
type = LibraryType.SAMPLES
inceptionYear = "2021"
description = "Samples for Compose integration with Lifecycle ViewModel"
diff --git a/lifecycle/lifecycle-viewmodel-ktx/api/2.6.0-beta02.txt b/lifecycle/lifecycle-viewmodel-ktx/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..1d1d247
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-ktx/api/2.6.0-beta02.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class ViewModelKt {
+ method public static kotlinx.coroutines.CoroutineScope getViewModelScope(androidx.lifecycle.ViewModel);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-viewmodel-ktx/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-viewmodel-ktx/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..1d1d247
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-ktx/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class ViewModelKt {
+ method public static kotlinx.coroutines.CoroutineScope getViewModelScope(androidx.lifecycle.ViewModel);
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-viewmodel-ktx/api/res-2.6.0-beta02.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to lifecycle/lifecycle-viewmodel-ktx/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-viewmodel-ktx/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-viewmodel-ktx/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..1d1d247
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-ktx/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public final class ViewModelKt {
+ method public static kotlinx.coroutines.CoroutineScope getViewModelScope(androidx.lifecycle.ViewModel);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-viewmodel-ktx/build.gradle b/lifecycle/lifecycle-viewmodel-ktx/build.gradle
index efaa638..dfb7838 100644
--- a/lifecycle/lifecycle-viewmodel-ktx/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-ktx/build.gradle
@@ -35,7 +35,7 @@
}
androidx {
- name = "Android Lifecycle ViewModel Kotlin Extensions"
+ name = "Lifecycle ViewModel Kotlin Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Kotlin extensions for 'viewmodel' artifact"
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/api/2.6.0-beta02.txt b/lifecycle/lifecycle-viewmodel-savedstate/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..c030c8a
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-savedstate/api/2.6.0-beta02.txt
@@ -0,0 +1,45 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public abstract class AbstractSavedStateViewModelFactory implements androidx.lifecycle.ViewModelProvider.Factory {
+ ctor public AbstractSavedStateViewModelFactory();
+ ctor public AbstractSavedStateViewModelFactory(androidx.savedstate.SavedStateRegistryOwner owner, android.os.Bundle? defaultArgs);
+ method protected abstract <T extends androidx.lifecycle.ViewModel> T create(String key, Class<T> modelClass, androidx.lifecycle.SavedStateHandle handle);
+ }
+
+ public final class SavedStateHandle {
+ ctor public SavedStateHandle(java.util.Map<java.lang.String,?> initialState);
+ ctor public SavedStateHandle();
+ method @MainThread public void clearSavedStateProvider(String key);
+ method @MainThread public operator boolean contains(String key);
+ method @MainThread public operator <T> T? get(String key);
+ method @MainThread public <T> androidx.lifecycle.MutableLiveData<T> getLiveData(String key);
+ method @MainThread public <T> androidx.lifecycle.MutableLiveData<T> getLiveData(String key, T? initialValue);
+ method @MainThread public <T> kotlinx.coroutines.flow.StateFlow<T> getStateFlow(String key, T? initialValue);
+ method @MainThread public java.util.Set<java.lang.String> keys();
+ method @MainThread public <T> T? remove(String key);
+ method @MainThread public operator <T> void set(String key, T? value);
+ method @MainThread public void setSavedStateProvider(String key, androidx.savedstate.SavedStateRegistry.SavedStateProvider provider);
+ field public static final androidx.lifecycle.SavedStateHandle.Companion Companion;
+ }
+
+ public static final class SavedStateHandle.Companion {
+ }
+
+ public final class SavedStateHandleSupport {
+ method @MainThread public static androidx.lifecycle.SavedStateHandle createSavedStateHandle(androidx.lifecycle.viewmodel.CreationExtras);
+ method @MainThread public static <T extends androidx.savedstate.SavedStateRegistryOwner & androidx.lifecycle.ViewModelStoreOwner> void enableSavedStateHandles(T);
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Key<android.os.Bundle> DEFAULT_ARGS_KEY;
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Key<androidx.savedstate.SavedStateRegistryOwner> SAVED_STATE_REGISTRY_OWNER_KEY;
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Key<androidx.lifecycle.ViewModelStoreOwner> VIEW_MODEL_STORE_OWNER_KEY;
+ }
+
+ public final class SavedStateViewModelFactory implements androidx.lifecycle.ViewModelProvider.Factory {
+ ctor public SavedStateViewModelFactory();
+ ctor public SavedStateViewModelFactory(android.app.Application? application, androidx.savedstate.SavedStateRegistryOwner owner);
+ ctor public SavedStateViewModelFactory(android.app.Application? application, androidx.savedstate.SavedStateRegistryOwner owner, android.os.Bundle? defaultArgs);
+ method public <T extends androidx.lifecycle.ViewModel> T create(String key, Class<T> modelClass);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-viewmodel-savedstate/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..c030c8a
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-savedstate/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,45 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public abstract class AbstractSavedStateViewModelFactory implements androidx.lifecycle.ViewModelProvider.Factory {
+ ctor public AbstractSavedStateViewModelFactory();
+ ctor public AbstractSavedStateViewModelFactory(androidx.savedstate.SavedStateRegistryOwner owner, android.os.Bundle? defaultArgs);
+ method protected abstract <T extends androidx.lifecycle.ViewModel> T create(String key, Class<T> modelClass, androidx.lifecycle.SavedStateHandle handle);
+ }
+
+ public final class SavedStateHandle {
+ ctor public SavedStateHandle(java.util.Map<java.lang.String,?> initialState);
+ ctor public SavedStateHandle();
+ method @MainThread public void clearSavedStateProvider(String key);
+ method @MainThread public operator boolean contains(String key);
+ method @MainThread public operator <T> T? get(String key);
+ method @MainThread public <T> androidx.lifecycle.MutableLiveData<T> getLiveData(String key);
+ method @MainThread public <T> androidx.lifecycle.MutableLiveData<T> getLiveData(String key, T? initialValue);
+ method @MainThread public <T> kotlinx.coroutines.flow.StateFlow<T> getStateFlow(String key, T? initialValue);
+ method @MainThread public java.util.Set<java.lang.String> keys();
+ method @MainThread public <T> T? remove(String key);
+ method @MainThread public operator <T> void set(String key, T? value);
+ method @MainThread public void setSavedStateProvider(String key, androidx.savedstate.SavedStateRegistry.SavedStateProvider provider);
+ field public static final androidx.lifecycle.SavedStateHandle.Companion Companion;
+ }
+
+ public static final class SavedStateHandle.Companion {
+ }
+
+ public final class SavedStateHandleSupport {
+ method @MainThread public static androidx.lifecycle.SavedStateHandle createSavedStateHandle(androidx.lifecycle.viewmodel.CreationExtras);
+ method @MainThread public static <T extends androidx.savedstate.SavedStateRegistryOwner & androidx.lifecycle.ViewModelStoreOwner> void enableSavedStateHandles(T);
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Key<android.os.Bundle> DEFAULT_ARGS_KEY;
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Key<androidx.savedstate.SavedStateRegistryOwner> SAVED_STATE_REGISTRY_OWNER_KEY;
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Key<androidx.lifecycle.ViewModelStoreOwner> VIEW_MODEL_STORE_OWNER_KEY;
+ }
+
+ public final class SavedStateViewModelFactory implements androidx.lifecycle.ViewModelProvider.Factory {
+ ctor public SavedStateViewModelFactory();
+ ctor public SavedStateViewModelFactory(android.app.Application? application, androidx.savedstate.SavedStateRegistryOwner owner);
+ ctor public SavedStateViewModelFactory(android.app.Application? application, androidx.savedstate.SavedStateRegistryOwner owner, android.os.Bundle? defaultArgs);
+ method public <T extends androidx.lifecycle.ViewModel> T create(String key, Class<T> modelClass);
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-viewmodel-savedstate/api/res-2.6.0-beta02.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to lifecycle/lifecycle-viewmodel-savedstate/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..c030c8a
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,45 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public abstract class AbstractSavedStateViewModelFactory implements androidx.lifecycle.ViewModelProvider.Factory {
+ ctor public AbstractSavedStateViewModelFactory();
+ ctor public AbstractSavedStateViewModelFactory(androidx.savedstate.SavedStateRegistryOwner owner, android.os.Bundle? defaultArgs);
+ method protected abstract <T extends androidx.lifecycle.ViewModel> T create(String key, Class<T> modelClass, androidx.lifecycle.SavedStateHandle handle);
+ }
+
+ public final class SavedStateHandle {
+ ctor public SavedStateHandle(java.util.Map<java.lang.String,?> initialState);
+ ctor public SavedStateHandle();
+ method @MainThread public void clearSavedStateProvider(String key);
+ method @MainThread public operator boolean contains(String key);
+ method @MainThread public operator <T> T? get(String key);
+ method @MainThread public <T> androidx.lifecycle.MutableLiveData<T> getLiveData(String key);
+ method @MainThread public <T> androidx.lifecycle.MutableLiveData<T> getLiveData(String key, T? initialValue);
+ method @MainThread public <T> kotlinx.coroutines.flow.StateFlow<T> getStateFlow(String key, T? initialValue);
+ method @MainThread public java.util.Set<java.lang.String> keys();
+ method @MainThread public <T> T? remove(String key);
+ method @MainThread public operator <T> void set(String key, T? value);
+ method @MainThread public void setSavedStateProvider(String key, androidx.savedstate.SavedStateRegistry.SavedStateProvider provider);
+ field public static final androidx.lifecycle.SavedStateHandle.Companion Companion;
+ }
+
+ public static final class SavedStateHandle.Companion {
+ }
+
+ public final class SavedStateHandleSupport {
+ method @MainThread public static androidx.lifecycle.SavedStateHandle createSavedStateHandle(androidx.lifecycle.viewmodel.CreationExtras);
+ method @MainThread public static <T extends androidx.savedstate.SavedStateRegistryOwner & androidx.lifecycle.ViewModelStoreOwner> void enableSavedStateHandles(T);
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Key<android.os.Bundle> DEFAULT_ARGS_KEY;
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Key<androidx.savedstate.SavedStateRegistryOwner> SAVED_STATE_REGISTRY_OWNER_KEY;
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Key<androidx.lifecycle.ViewModelStoreOwner> VIEW_MODEL_STORE_OWNER_KEY;
+ }
+
+ public final class SavedStateViewModelFactory implements androidx.lifecycle.ViewModelProvider.Factory {
+ ctor public SavedStateViewModelFactory();
+ ctor public SavedStateViewModelFactory(android.app.Application? application, androidx.savedstate.SavedStateRegistryOwner owner);
+ ctor public SavedStateViewModelFactory(android.app.Application? application, androidx.savedstate.SavedStateRegistryOwner owner, android.os.Bundle? defaultArgs);
+ method public <T extends androidx.lifecycle.ViewModel> T create(String key, Class<T> modelClass);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
index ec45470..c2144f0 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
@@ -51,7 +51,7 @@
}
androidx {
- name = "Android Lifecycle ViewModel with SavedState"
+ name = "Lifecycle ViewModel with SavedState"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Android Lifecycle ViewModel"
diff --git a/lifecycle/lifecycle-viewmodel/api/2.6.0-beta02.txt b/lifecycle/lifecycle-viewmodel/api/2.6.0-beta02.txt
new file mode 100644
index 0000000..f8457f6
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel/api/2.6.0-beta02.txt
@@ -0,0 +1,136 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public class AndroidViewModel extends androidx.lifecycle.ViewModel {
+ ctor public AndroidViewModel(android.app.Application application);
+ method public <T extends android.app.Application> T getApplication();
+ }
+
+ public interface HasDefaultViewModelProviderFactory {
+ method public default androidx.lifecycle.viewmodel.CreationExtras getDefaultViewModelCreationExtras();
+ method public androidx.lifecycle.ViewModelProvider.Factory getDefaultViewModelProviderFactory();
+ property public default androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
+ property public abstract androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
+ }
+
+ public abstract class ViewModel {
+ ctor public ViewModel();
+ ctor public ViewModel(java.io.Closeable!...);
+ method public void addCloseable(java.io.Closeable);
+ method protected void onCleared();
+ }
+
+ public final class ViewModelLazy<VM extends androidx.lifecycle.ViewModel> implements kotlin.Lazy<VM> {
+ ctor public ViewModelLazy(kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory> factoryProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras> extrasProducer);
+ ctor public ViewModelLazy(kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory> factoryProducer);
+ method public VM getValue();
+ method public boolean isInitialized();
+ property public VM value;
+ }
+
+ public class ViewModelProvider {
+ ctor public ViewModelProvider(androidx.lifecycle.ViewModelStore store, androidx.lifecycle.ViewModelProvider.Factory factory, optional androidx.lifecycle.viewmodel.CreationExtras defaultCreationExtras);
+ ctor public ViewModelProvider(androidx.lifecycle.ViewModelStore store, androidx.lifecycle.ViewModelProvider.Factory factory);
+ ctor public ViewModelProvider(androidx.lifecycle.ViewModelStoreOwner owner);
+ ctor public ViewModelProvider(androidx.lifecycle.ViewModelStoreOwner owner, androidx.lifecycle.ViewModelProvider.Factory factory);
+ method @MainThread public operator <T extends androidx.lifecycle.ViewModel> T get(Class<T> modelClass);
+ method @MainThread public operator <T extends androidx.lifecycle.ViewModel> T get(String key, Class<T> modelClass);
+ }
+
+ public static class ViewModelProvider.AndroidViewModelFactory extends androidx.lifecycle.ViewModelProvider.NewInstanceFactory {
+ ctor public ViewModelProvider.AndroidViewModelFactory();
+ ctor public ViewModelProvider.AndroidViewModelFactory(android.app.Application application);
+ method public static final androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory getInstance(android.app.Application application);
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Key<android.app.Application> APPLICATION_KEY;
+ field public static final androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion Companion;
+ }
+
+ public static final class ViewModelProvider.AndroidViewModelFactory.Companion {
+ method public androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory getInstance(android.app.Application application);
+ }
+
+ public static interface ViewModelProvider.Factory {
+ method public default <T extends androidx.lifecycle.ViewModel> T create(Class<T> modelClass);
+ method public default <T extends androidx.lifecycle.ViewModel> T create(Class<T> modelClass, androidx.lifecycle.viewmodel.CreationExtras extras);
+ method public default static androidx.lifecycle.ViewModelProvider.Factory from(androidx.lifecycle.viewmodel.ViewModelInitializer<?>... initializers);
+ field public static final androidx.lifecycle.ViewModelProvider.Factory.Companion Companion;
+ }
+
+ public static final class ViewModelProvider.Factory.Companion {
+ method public androidx.lifecycle.ViewModelProvider.Factory from(androidx.lifecycle.viewmodel.ViewModelInitializer<?>... initializers);
+ }
+
+ public static class ViewModelProvider.NewInstanceFactory implements androidx.lifecycle.ViewModelProvider.Factory {
+ ctor public ViewModelProvider.NewInstanceFactory();
+ field public static final androidx.lifecycle.ViewModelProvider.NewInstanceFactory.Companion Companion;
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Key<java.lang.String> VIEW_MODEL_KEY;
+ }
+
+ public static final class ViewModelProvider.NewInstanceFactory.Companion {
+ }
+
+ public final class ViewModelProviderGetKt {
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> VM get(androidx.lifecycle.ViewModelProvider);
+ }
+
+ public class ViewModelStore {
+ ctor public ViewModelStore();
+ method public final void clear();
+ }
+
+ public interface ViewModelStoreOwner {
+ method public androidx.lifecycle.ViewModelStore getViewModelStore();
+ property public abstract androidx.lifecycle.ViewModelStore viewModelStore;
+ }
+
+ public final class ViewTreeViewModelKt {
+ method @Deprecated public static androidx.lifecycle.ViewModelStoreOwner? findViewTreeViewModelStoreOwner(android.view.View view);
+ }
+
+ public final class ViewTreeViewModelStoreOwner {
+ method public static androidx.lifecycle.ViewModelStoreOwner? get(android.view.View);
+ method public static void set(android.view.View, androidx.lifecycle.ViewModelStoreOwner? viewModelStoreOwner);
+ }
+
+}
+
+package androidx.lifecycle.viewmodel {
+
+ public abstract class CreationExtras {
+ method public abstract operator <T> T? get(androidx.lifecycle.viewmodel.CreationExtras.Key<T> key);
+ }
+
+ public static final class CreationExtras.Empty extends androidx.lifecycle.viewmodel.CreationExtras {
+ method public <T> T? get(androidx.lifecycle.viewmodel.CreationExtras.Key<T> key);
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Empty INSTANCE;
+ }
+
+ public static interface CreationExtras.Key<T> {
+ }
+
+ @androidx.lifecycle.viewmodel.ViewModelFactoryDsl public final class InitializerViewModelFactoryBuilder {
+ ctor public InitializerViewModelFactoryBuilder();
+ method public <T extends androidx.lifecycle.ViewModel> void addInitializer(kotlin.reflect.KClass<T> clazz, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends T> initializer);
+ method public androidx.lifecycle.ViewModelProvider.Factory build();
+ }
+
+ public final class InitializerViewModelFactoryKt {
+ method public static inline <reified VM extends androidx.lifecycle.ViewModel> void initializer(androidx.lifecycle.viewmodel.InitializerViewModelFactoryBuilder, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends VM> initializer);
+ method public static inline androidx.lifecycle.ViewModelProvider.Factory viewModelFactory(kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.InitializerViewModelFactoryBuilder,kotlin.Unit> builder);
+ }
+
+ public final class MutableCreationExtras extends androidx.lifecycle.viewmodel.CreationExtras {
+ ctor public MutableCreationExtras(optional androidx.lifecycle.viewmodel.CreationExtras initialExtras);
+ method public <T> T? get(androidx.lifecycle.viewmodel.CreationExtras.Key<T> key);
+ method public operator <T> void set(androidx.lifecycle.viewmodel.CreationExtras.Key<T> key, T? t);
+ }
+
+ @kotlin.DslMarker public @interface ViewModelFactoryDsl {
+ }
+
+ public final class ViewModelInitializer<T extends androidx.lifecycle.ViewModel> {
+ ctor public ViewModelInitializer(Class<T> clazz, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends T> initializer);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_2.6.0-beta02.txt b/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_2.6.0-beta02.txt
new file mode 100644
index 0000000..f8457f6
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_2.6.0-beta02.txt
@@ -0,0 +1,136 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public class AndroidViewModel extends androidx.lifecycle.ViewModel {
+ ctor public AndroidViewModel(android.app.Application application);
+ method public <T extends android.app.Application> T getApplication();
+ }
+
+ public interface HasDefaultViewModelProviderFactory {
+ method public default androidx.lifecycle.viewmodel.CreationExtras getDefaultViewModelCreationExtras();
+ method public androidx.lifecycle.ViewModelProvider.Factory getDefaultViewModelProviderFactory();
+ property public default androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
+ property public abstract androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
+ }
+
+ public abstract class ViewModel {
+ ctor public ViewModel();
+ ctor public ViewModel(java.io.Closeable!...);
+ method public void addCloseable(java.io.Closeable);
+ method protected void onCleared();
+ }
+
+ public final class ViewModelLazy<VM extends androidx.lifecycle.ViewModel> implements kotlin.Lazy<VM> {
+ ctor public ViewModelLazy(kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory> factoryProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras> extrasProducer);
+ ctor public ViewModelLazy(kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory> factoryProducer);
+ method public VM getValue();
+ method public boolean isInitialized();
+ property public VM value;
+ }
+
+ public class ViewModelProvider {
+ ctor public ViewModelProvider(androidx.lifecycle.ViewModelStore store, androidx.lifecycle.ViewModelProvider.Factory factory, optional androidx.lifecycle.viewmodel.CreationExtras defaultCreationExtras);
+ ctor public ViewModelProvider(androidx.lifecycle.ViewModelStore store, androidx.lifecycle.ViewModelProvider.Factory factory);
+ ctor public ViewModelProvider(androidx.lifecycle.ViewModelStoreOwner owner);
+ ctor public ViewModelProvider(androidx.lifecycle.ViewModelStoreOwner owner, androidx.lifecycle.ViewModelProvider.Factory factory);
+ method @MainThread public operator <T extends androidx.lifecycle.ViewModel> T get(Class<T> modelClass);
+ method @MainThread public operator <T extends androidx.lifecycle.ViewModel> T get(String key, Class<T> modelClass);
+ }
+
+ public static class ViewModelProvider.AndroidViewModelFactory extends androidx.lifecycle.ViewModelProvider.NewInstanceFactory {
+ ctor public ViewModelProvider.AndroidViewModelFactory();
+ ctor public ViewModelProvider.AndroidViewModelFactory(android.app.Application application);
+ method public static final androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory getInstance(android.app.Application application);
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Key<android.app.Application> APPLICATION_KEY;
+ field public static final androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion Companion;
+ }
+
+ public static final class ViewModelProvider.AndroidViewModelFactory.Companion {
+ method public androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory getInstance(android.app.Application application);
+ }
+
+ public static interface ViewModelProvider.Factory {
+ method public default <T extends androidx.lifecycle.ViewModel> T create(Class<T> modelClass);
+ method public default <T extends androidx.lifecycle.ViewModel> T create(Class<T> modelClass, androidx.lifecycle.viewmodel.CreationExtras extras);
+ method public default static androidx.lifecycle.ViewModelProvider.Factory from(androidx.lifecycle.viewmodel.ViewModelInitializer<?>... initializers);
+ field public static final androidx.lifecycle.ViewModelProvider.Factory.Companion Companion;
+ }
+
+ public static final class ViewModelProvider.Factory.Companion {
+ method public androidx.lifecycle.ViewModelProvider.Factory from(androidx.lifecycle.viewmodel.ViewModelInitializer<?>... initializers);
+ }
+
+ public static class ViewModelProvider.NewInstanceFactory implements androidx.lifecycle.ViewModelProvider.Factory {
+ ctor public ViewModelProvider.NewInstanceFactory();
+ field public static final androidx.lifecycle.ViewModelProvider.NewInstanceFactory.Companion Companion;
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Key<java.lang.String> VIEW_MODEL_KEY;
+ }
+
+ public static final class ViewModelProvider.NewInstanceFactory.Companion {
+ }
+
+ public final class ViewModelProviderGetKt {
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> VM get(androidx.lifecycle.ViewModelProvider);
+ }
+
+ public class ViewModelStore {
+ ctor public ViewModelStore();
+ method public final void clear();
+ }
+
+ public interface ViewModelStoreOwner {
+ method public androidx.lifecycle.ViewModelStore getViewModelStore();
+ property public abstract androidx.lifecycle.ViewModelStore viewModelStore;
+ }
+
+ public final class ViewTreeViewModelKt {
+ method @Deprecated public static androidx.lifecycle.ViewModelStoreOwner? findViewTreeViewModelStoreOwner(android.view.View view);
+ }
+
+ public final class ViewTreeViewModelStoreOwner {
+ method public static androidx.lifecycle.ViewModelStoreOwner? get(android.view.View);
+ method public static void set(android.view.View, androidx.lifecycle.ViewModelStoreOwner? viewModelStoreOwner);
+ }
+
+}
+
+package androidx.lifecycle.viewmodel {
+
+ public abstract class CreationExtras {
+ method public abstract operator <T> T? get(androidx.lifecycle.viewmodel.CreationExtras.Key<T> key);
+ }
+
+ public static final class CreationExtras.Empty extends androidx.lifecycle.viewmodel.CreationExtras {
+ method public <T> T? get(androidx.lifecycle.viewmodel.CreationExtras.Key<T> key);
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Empty INSTANCE;
+ }
+
+ public static interface CreationExtras.Key<T> {
+ }
+
+ @androidx.lifecycle.viewmodel.ViewModelFactoryDsl public final class InitializerViewModelFactoryBuilder {
+ ctor public InitializerViewModelFactoryBuilder();
+ method public <T extends androidx.lifecycle.ViewModel> void addInitializer(kotlin.reflect.KClass<T> clazz, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends T> initializer);
+ method public androidx.lifecycle.ViewModelProvider.Factory build();
+ }
+
+ public final class InitializerViewModelFactoryKt {
+ method public static inline <reified VM extends androidx.lifecycle.ViewModel> void initializer(androidx.lifecycle.viewmodel.InitializerViewModelFactoryBuilder, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends VM> initializer);
+ method public static inline androidx.lifecycle.ViewModelProvider.Factory viewModelFactory(kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.InitializerViewModelFactoryBuilder,kotlin.Unit> builder);
+ }
+
+ public final class MutableCreationExtras extends androidx.lifecycle.viewmodel.CreationExtras {
+ ctor public MutableCreationExtras(optional androidx.lifecycle.viewmodel.CreationExtras initialExtras);
+ method public <T> T? get(androidx.lifecycle.viewmodel.CreationExtras.Key<T> key);
+ method public operator <T> void set(androidx.lifecycle.viewmodel.CreationExtras.Key<T> key, T? t);
+ }
+
+ @kotlin.DslMarker public @interface ViewModelFactoryDsl {
+ }
+
+ public final class ViewModelInitializer<T extends androidx.lifecycle.ViewModel> {
+ ctor public ViewModelInitializer(Class<T> clazz, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends T> initializer);
+ }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/lifecycle/lifecycle-viewmodel/api/res-2.6.0-beta02.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to lifecycle/lifecycle-viewmodel/api/res-2.6.0-beta02.txt
diff --git a/lifecycle/lifecycle-viewmodel/api/restricted_2.6.0-beta02.txt b/lifecycle/lifecycle-viewmodel/api/restricted_2.6.0-beta02.txt
new file mode 100644
index 0000000..f8457f6
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel/api/restricted_2.6.0-beta02.txt
@@ -0,0 +1,136 @@
+// Signature format: 4.0
+package androidx.lifecycle {
+
+ public class AndroidViewModel extends androidx.lifecycle.ViewModel {
+ ctor public AndroidViewModel(android.app.Application application);
+ method public <T extends android.app.Application> T getApplication();
+ }
+
+ public interface HasDefaultViewModelProviderFactory {
+ method public default androidx.lifecycle.viewmodel.CreationExtras getDefaultViewModelCreationExtras();
+ method public androidx.lifecycle.ViewModelProvider.Factory getDefaultViewModelProviderFactory();
+ property public default androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
+ property public abstract androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
+ }
+
+ public abstract class ViewModel {
+ ctor public ViewModel();
+ ctor public ViewModel(java.io.Closeable!...);
+ method public void addCloseable(java.io.Closeable);
+ method protected void onCleared();
+ }
+
+ public final class ViewModelLazy<VM extends androidx.lifecycle.ViewModel> implements kotlin.Lazy<VM> {
+ ctor public ViewModelLazy(kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory> factoryProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras> extrasProducer);
+ ctor public ViewModelLazy(kotlin.reflect.KClass<VM> viewModelClass, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStore> storeProducer, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory> factoryProducer);
+ method public VM getValue();
+ method public boolean isInitialized();
+ property public VM value;
+ }
+
+ public class ViewModelProvider {
+ ctor public ViewModelProvider(androidx.lifecycle.ViewModelStore store, androidx.lifecycle.ViewModelProvider.Factory factory, optional androidx.lifecycle.viewmodel.CreationExtras defaultCreationExtras);
+ ctor public ViewModelProvider(androidx.lifecycle.ViewModelStore store, androidx.lifecycle.ViewModelProvider.Factory factory);
+ ctor public ViewModelProvider(androidx.lifecycle.ViewModelStoreOwner owner);
+ ctor public ViewModelProvider(androidx.lifecycle.ViewModelStoreOwner owner, androidx.lifecycle.ViewModelProvider.Factory factory);
+ method @MainThread public operator <T extends androidx.lifecycle.ViewModel> T get(Class<T> modelClass);
+ method @MainThread public operator <T extends androidx.lifecycle.ViewModel> T get(String key, Class<T> modelClass);
+ }
+
+ public static class ViewModelProvider.AndroidViewModelFactory extends androidx.lifecycle.ViewModelProvider.NewInstanceFactory {
+ ctor public ViewModelProvider.AndroidViewModelFactory();
+ ctor public ViewModelProvider.AndroidViewModelFactory(android.app.Application application);
+ method public static final androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory getInstance(android.app.Application application);
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Key<android.app.Application> APPLICATION_KEY;
+ field public static final androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion Companion;
+ }
+
+ public static final class ViewModelProvider.AndroidViewModelFactory.Companion {
+ method public androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory getInstance(android.app.Application application);
+ }
+
+ public static interface ViewModelProvider.Factory {
+ method public default <T extends androidx.lifecycle.ViewModel> T create(Class<T> modelClass);
+ method public default <T extends androidx.lifecycle.ViewModel> T create(Class<T> modelClass, androidx.lifecycle.viewmodel.CreationExtras extras);
+ method public default static androidx.lifecycle.ViewModelProvider.Factory from(androidx.lifecycle.viewmodel.ViewModelInitializer<?>... initializers);
+ field public static final androidx.lifecycle.ViewModelProvider.Factory.Companion Companion;
+ }
+
+ public static final class ViewModelProvider.Factory.Companion {
+ method public androidx.lifecycle.ViewModelProvider.Factory from(androidx.lifecycle.viewmodel.ViewModelInitializer<?>... initializers);
+ }
+
+ public static class ViewModelProvider.NewInstanceFactory implements androidx.lifecycle.ViewModelProvider.Factory {
+ ctor public ViewModelProvider.NewInstanceFactory();
+ field public static final androidx.lifecycle.ViewModelProvider.NewInstanceFactory.Companion Companion;
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Key<java.lang.String> VIEW_MODEL_KEY;
+ }
+
+ public static final class ViewModelProvider.NewInstanceFactory.Companion {
+ }
+
+ public final class ViewModelProviderGetKt {
+ method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> VM get(androidx.lifecycle.ViewModelProvider);
+ }
+
+ public class ViewModelStore {
+ ctor public ViewModelStore();
+ method public final void clear();
+ }
+
+ public interface ViewModelStoreOwner {
+ method public androidx.lifecycle.ViewModelStore getViewModelStore();
+ property public abstract androidx.lifecycle.ViewModelStore viewModelStore;
+ }
+
+ public final class ViewTreeViewModelKt {
+ method @Deprecated public static androidx.lifecycle.ViewModelStoreOwner? findViewTreeViewModelStoreOwner(android.view.View view);
+ }
+
+ public final class ViewTreeViewModelStoreOwner {
+ method public static androidx.lifecycle.ViewModelStoreOwner? get(android.view.View);
+ method public static void set(android.view.View, androidx.lifecycle.ViewModelStoreOwner? viewModelStoreOwner);
+ }
+
+}
+
+package androidx.lifecycle.viewmodel {
+
+ public abstract class CreationExtras {
+ method public abstract operator <T> T? get(androidx.lifecycle.viewmodel.CreationExtras.Key<T> key);
+ }
+
+ public static final class CreationExtras.Empty extends androidx.lifecycle.viewmodel.CreationExtras {
+ method public <T> T? get(androidx.lifecycle.viewmodel.CreationExtras.Key<T> key);
+ field public static final androidx.lifecycle.viewmodel.CreationExtras.Empty INSTANCE;
+ }
+
+ public static interface CreationExtras.Key<T> {
+ }
+
+ @androidx.lifecycle.viewmodel.ViewModelFactoryDsl public final class InitializerViewModelFactoryBuilder {
+ ctor public InitializerViewModelFactoryBuilder();
+ method public <T extends androidx.lifecycle.ViewModel> void addInitializer(kotlin.reflect.KClass<T> clazz, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends T> initializer);
+ method public androidx.lifecycle.ViewModelProvider.Factory build();
+ }
+
+ public final class InitializerViewModelFactoryKt {
+ method public static inline <reified VM extends androidx.lifecycle.ViewModel> void initializer(androidx.lifecycle.viewmodel.InitializerViewModelFactoryBuilder, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends VM> initializer);
+ method public static inline androidx.lifecycle.ViewModelProvider.Factory viewModelFactory(kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.InitializerViewModelFactoryBuilder,kotlin.Unit> builder);
+ }
+
+ public final class MutableCreationExtras extends androidx.lifecycle.viewmodel.CreationExtras {
+ ctor public MutableCreationExtras(optional androidx.lifecycle.viewmodel.CreationExtras initialExtras);
+ method public <T> T? get(androidx.lifecycle.viewmodel.CreationExtras.Key<T> key);
+ method public operator <T> void set(androidx.lifecycle.viewmodel.CreationExtras.Key<T> key, T? t);
+ }
+
+ @kotlin.DslMarker public @interface ViewModelFactoryDsl {
+ }
+
+ public final class ViewModelInitializer<T extends androidx.lifecycle.ViewModel> {
+ ctor public ViewModelInitializer(Class<T> clazz, kotlin.jvm.functions.Function1<? super androidx.lifecycle.viewmodel.CreationExtras,? extends T> initializer);
+ }
+
+}
+
diff --git a/lifecycle/lifecycle-viewmodel/build.gradle b/lifecycle/lifecycle-viewmodel/build.gradle
index 125c988..4c2db52 100644
--- a/lifecycle/lifecycle-viewmodel/build.gradle
+++ b/lifecycle/lifecycle-viewmodel/build.gradle
@@ -48,7 +48,7 @@
}
androidx {
- name = "Android Lifecycle ViewModel"
+ name = "Lifecycle ViewModel"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Lifecycle ViewModel"
diff --git a/loader/loader/build.gradle b/loader/loader/build.gradle
index e9ff426..5b46db6 100644
--- a/loader/loader/build.gradle
+++ b/loader/loader/build.gradle
@@ -23,7 +23,7 @@
}
androidx {
- name = "Android Support Library loader"
+ name = "loader"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2011"
description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\'t a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/media/media/api/current.txt b/media/media/api/current.txt
index 3e65d18..cf7a91d 100644
--- a/media/media/api/current.txt
+++ b/media/media/api/current.txt
@@ -690,6 +690,7 @@
method public static android.support.v4.media.session.MediaSessionCompat.Token! getMediaSession(android.app.Notification!);
method public androidx.media.app.NotificationCompat.MediaStyle! setCancelButtonIntent(android.app.PendingIntent!);
method public androidx.media.app.NotificationCompat.MediaStyle! setMediaSession(android.support.v4.media.session.MediaSessionCompat.Token!);
+ method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public androidx.media.app.NotificationCompat.MediaStyle setRemotePlaybackInfo(CharSequence, @DrawableRes int, android.app.PendingIntent?);
method public androidx.media.app.NotificationCompat.MediaStyle! setShowActionsInCompactView(int...);
method public androidx.media.app.NotificationCompat.MediaStyle! setShowCancelButton(boolean);
}
diff --git a/media/media/api/public_plus_experimental_current.txt b/media/media/api/public_plus_experimental_current.txt
index 3e65d18..cf7a91d 100644
--- a/media/media/api/public_plus_experimental_current.txt
+++ b/media/media/api/public_plus_experimental_current.txt
@@ -690,6 +690,7 @@
method public static android.support.v4.media.session.MediaSessionCompat.Token! getMediaSession(android.app.Notification!);
method public androidx.media.app.NotificationCompat.MediaStyle! setCancelButtonIntent(android.app.PendingIntent!);
method public androidx.media.app.NotificationCompat.MediaStyle! setMediaSession(android.support.v4.media.session.MediaSessionCompat.Token!);
+ method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public androidx.media.app.NotificationCompat.MediaStyle setRemotePlaybackInfo(CharSequence, @DrawableRes int, android.app.PendingIntent?);
method public androidx.media.app.NotificationCompat.MediaStyle! setShowActionsInCompactView(int...);
method public androidx.media.app.NotificationCompat.MediaStyle! setShowCancelButton(boolean);
}
diff --git a/media/media/api/restricted_current.txt b/media/media/api/restricted_current.txt
index 35c24b8..4d68abb 100644
--- a/media/media/api/restricted_current.txt
+++ b/media/media/api/restricted_current.txt
@@ -728,6 +728,7 @@
method public static android.support.v4.media.session.MediaSessionCompat.Token! getMediaSession(android.app.Notification!);
method public androidx.media.app.NotificationCompat.MediaStyle! setCancelButtonIntent(android.app.PendingIntent!);
method public androidx.media.app.NotificationCompat.MediaStyle! setMediaSession(android.support.v4.media.session.MediaSessionCompat.Token!);
+ method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public androidx.media.app.NotificationCompat.MediaStyle setRemotePlaybackInfo(CharSequence, @DrawableRes int, android.app.PendingIntent?);
method public androidx.media.app.NotificationCompat.MediaStyle! setShowActionsInCompactView(int...);
method public androidx.media.app.NotificationCompat.MediaStyle! setShowCancelButton(boolean);
}
diff --git a/media/media/build.gradle b/media/media/build.gradle
index 91a6177..1523950 100644
--- a/media/media/build.gradle
+++ b/media/media/build.gradle
@@ -25,6 +25,7 @@
api("androidx.core:core:1.6.0")
implementation("androidx.annotation:annotation:1.2.0")
implementation("androidx.collection:collection:1.1.0")
+ implementation("androidx.core:core:1.9.0")
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
@@ -51,7 +52,7 @@
}
androidx {
- name = "Android Support Library media compat"
+ name = "Media"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.MEDIA
inceptionYear = "2011"
diff --git a/media/media/lint-baseline.xml b/media/media/lint-baseline.xml
index 7760b0e..83d9e78 100644
--- a/media/media/lint-baseline.xml
+++ b/media/media/lint-baseline.xml
@@ -1,5 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-beta03" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-beta03)" variant="all" version="8.0.0-beta03">
+<issues format="6" by="lint 8.1.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta01)" variant="all" version="8.1.0-beta01">
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/media/app/NotificationCompat.java"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/media/app/NotificationCompat.java"/>
+ </issue>
<issue
id="RequireUnstableAidlAnnotation"
diff --git a/media/media/src/main/java/androidx/media/app/NotificationCompat.java b/media/media/src/main/java/androidx/media/app/NotificationCompat.java
index e5c36ad..be90897 100644
--- a/media/media/src/main/java/androidx/media/app/NotificationCompat.java
+++ b/media/media/src/main/java/androidx/media/app/NotificationCompat.java
@@ -16,9 +16,12 @@
package androidx.media.app;
+import static android.Manifest.permission.MEDIA_CONTENT_CONTROL;
+
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.core.app.NotificationCompat.COLOR_DEFAULT;
+import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.PendingIntent;
import android.media.session.MediaSession;
@@ -31,10 +34,16 @@
import android.widget.RemoteViews;
import androidx.annotation.DoNotInline;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
+import androidx.annotation.RequiresPermission;
import androidx.annotation.RestrictTo;
import androidx.core.app.BundleCompat;
import androidx.core.app.NotificationBuilderWithBuilderAccessor;
+import androidx.core.os.BuildCompat;
import androidx.media.R;
/**
@@ -133,6 +142,10 @@
MediaSessionCompat.Token mToken;
boolean mShowCancelButton;
PendingIntent mCancelButtonIntent;
+ CharSequence mDeviceName;
+ int mDeviceIcon;
+ PendingIntent mDeviceIntent;
+ boolean mShowRemotePlaybackInfo = false;
public MediaStyle() {
}
@@ -162,6 +175,36 @@
}
/**
+ * For media notifications associated with playback on a remote device, provide device
+ * information that will replace the default values for the output switcher chip on the
+ * media control, as well as an intent to use when the output switcher chip is tapped,
+ * on devices where this is supported.
+ * <p>
+ * This method is intended for system applications to provide information and/or
+ * functionality that would otherwise be unavailable to the default output switcher because
+ * the media originated on a remote device.
+ * <p>
+ * Also note that this method is a no-op when running on Tiramisu or less.
+ *
+ * @param deviceName The name of the remote device to display.
+ * @param iconResource Icon resource, of size 12, representing the device.
+ * @param chipIntent PendingIntent to send when the output switcher is tapped. May be
+ * {@code null}, in which case the output switcher will be disabled.
+ * This intent should open an Activity or it will be ignored.
+ * @return MediaStyle
+ */
+ @RequiresPermission(MEDIA_CONTENT_CONTROL)
+ @NonNull
+ public MediaStyle setRemotePlaybackInfo(@NonNull CharSequence deviceName,
+ @DrawableRes int iconResource, @Nullable PendingIntent chipIntent) {
+ mDeviceName = deviceName;
+ mDeviceIcon = iconResource;
+ mDeviceIntent = chipIntent;
+ mShowRemotePlaybackInfo = true;
+ return this;
+ }
+
+ /**
* Sets whether a cancel button at the top right should be shown in the notification on
* platforms before Lollipop.
*
@@ -205,10 +248,17 @@
/**
*/
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
@RestrictTo(LIBRARY)
@Override
public void apply(NotificationBuilderWithBuilderAccessor builder) {
- if (Build.VERSION.SDK_INT >= 21) {
+ if (BuildCompat.isAtLeastU()) {
+ Api21Impl.setMediaStyle(builder.getBuilder(),
+ Api21Impl.fillInMediaStyle(Api34Impl.setRemotePlaybackInfo(
+ Api21Impl.createMediaStyle(), mDeviceName, mDeviceIcon,
+ mDeviceIntent, mShowRemotePlaybackInfo),
+ mActionsToShowInCompact, mToken));
+ } else if (Build.VERSION.SDK_INT >= 21) {
Api21Impl.setMediaStyle(builder.getBuilder(),
Api21Impl.fillInMediaStyle(Api21Impl.createMediaStyle(),
mActionsToShowInCompact, mToken));
@@ -370,10 +420,17 @@
/**
*/
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
@RestrictTo(LIBRARY)
@Override
public void apply(NotificationBuilderWithBuilderAccessor builder) {
- if (Build.VERSION.SDK_INT >= 24) {
+ if (BuildCompat.isAtLeastU()) {
+ Api21Impl.setMediaStyle(builder.getBuilder(), Api21Impl.fillInMediaStyle(
+ Api34Impl.setRemotePlaybackInfo(
+ Api24Impl.createDecoratedMediaCustomViewStyle(), mDeviceName,
+ mDeviceIcon, mDeviceIntent, mShowRemotePlaybackInfo),
+ mActionsToShowInCompact, mToken));
+ } else if (Build.VERSION.SDK_INT >= 24) {
Api21Impl.setMediaStyle(builder.getBuilder(),
Api21Impl.fillInMediaStyle(Api24Impl.createDecoratedMediaCustomViewStyle(),
mActionsToShowInCompact, mToken));
@@ -544,4 +601,24 @@
return new Notification.DecoratedMediaCustomViewStyle();
}
}
-}
+
+ @RequiresApi(34)
+ private static class Api34Impl {
+
+ private Api34Impl() {}
+
+ @SuppressLint({"MissingPermission"})
+ @DoNotInline
+ static Notification.MediaStyle setRemotePlaybackInfo(Notification.MediaStyle style,
+ @NonNull CharSequence deviceName, @DrawableRes int iconResource,
+ @Nullable PendingIntent chipIntent, Boolean showRemotePlaybackInfo) {
+ // Suppress @RequiresPermission(MEDIA_CONTENT_CONTROL) because the API is only used
+ // if showRemotePlaybackInfo is set to true. This only happens for callers to
+ // NotificationCompat#setRemotePlaybackInfo.
+ if (showRemotePlaybackInfo) {
+ style.setRemotePlaybackInfo(deviceName, iconResource, chipIntent);
+ }
+ return style;
+ }
+ }
+}
\ No newline at end of file
diff --git a/media2/media2-common/build.gradle b/media2/media2-common/build.gradle
index 8c0570f..46c5577 100644
--- a/media2/media2-common/build.gradle
+++ b/media2/media2-common/build.gradle
@@ -62,7 +62,7 @@
}
androidx {
- name = "AndroidX media2 common library"
+ name = "Media2 Common"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Media2 Common"
diff --git a/media2/media2-exoplayer/build.gradle b/media2/media2-exoplayer/build.gradle
index 291a8bc..c7873df 100644
--- a/media2/media2-exoplayer/build.gradle
+++ b/media2/media2-exoplayer/build.gradle
@@ -35,7 +35,7 @@
}
androidx {
- name = "Media2 repackaged ExoPlayer dependency"
+ name = "Media2 ExoPlayer"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Repackaged ExoPlayer for 'media2' artifact"
diff --git a/media2/media2-player/build.gradle b/media2/media2-player/build.gradle
index c11da0f..ca8a752 100644
--- a/media2/media2-player/build.gradle
+++ b/media2/media2-player/build.gradle
@@ -49,7 +49,7 @@
}
androidx {
- name = "AndroidX media2 player library"
+ name = "Media2 Player"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Media2 Player"
diff --git a/media2/media2-session/build.gradle b/media2/media2-session/build.gradle
index 8830b4f..66b502d 100644
--- a/media2/media2-session/build.gradle
+++ b/media2/media2-session/build.gradle
@@ -56,7 +56,7 @@
}
androidx {
- name = "AndroidX media2 session library"
+ name = "Media2 Session"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Media2 Session"
diff --git a/media2/media2-widget/build.gradle b/media2/media2-widget/build.gradle
index d4452b3..ceee35f 100644
--- a/media2/media2-widget/build.gradle
+++ b/media2/media2-widget/build.gradle
@@ -56,7 +56,7 @@
}
androidx {
- name = "AndroidX media2 widget library"
+ name = "Media2 Widget"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "AndroidX Media2 Widget"
diff --git a/mediarouter/mediarouter-testing/build.gradle b/mediarouter/mediarouter-testing/build.gradle
index 5d2fbf6..0e39af2 100644
--- a/mediarouter/mediarouter-testing/build.gradle
+++ b/mediarouter/mediarouter-testing/build.gradle
@@ -27,7 +27,7 @@
}
androidx {
- name = "AndroidX MediaRouter Testing"
+ name = "MediaRouter Testing"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2021"
description = "Test utilities for AndroidX MediaRouter"
diff --git a/mediarouter/mediarouter/api/current.txt b/mediarouter/mediarouter/api/current.txt
index 8446474..f29ae85 100644
--- a/mediarouter/mediarouter/api/current.txt
+++ b/mediarouter/mediarouter/api/current.txt
@@ -170,8 +170,10 @@
method public android.os.Bundle asBundle();
method public boolean canDisconnectAndKeepPlaying();
method public static androidx.mediarouter.media.MediaRouteDescriptor? fromBundle(android.os.Bundle?);
+ method public java.util.Set<java.lang.String!> getAllowedPackages();
method public int getConnectionState();
method public java.util.List<android.content.IntentFilter!> getControlFilters();
+ method public java.util.Set<java.lang.String!> getDeduplicationIds();
method public String? getDescription();
method public int getDeviceType();
method public android.os.Bundle? getExtras();
@@ -189,6 +191,7 @@
method public boolean isDynamicGroupRoute();
method public boolean isEnabled();
method public boolean isValid();
+ method public boolean isVisibilityPublic();
}
public static final class MediaRouteDescriptor.Builder {
@@ -201,6 +204,7 @@
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setCanDisconnect(boolean);
method @Deprecated public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnecting(boolean);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnectionState(int);
+ method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDeduplicationIds(java.util.Set<java.lang.String!>);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDescription(String?);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDeviceType(int);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setEnabled(boolean);
@@ -213,6 +217,8 @@
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setPlaybackType(int);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setPresentationDisplayId(int);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setSettingsActivity(android.content.IntentSender?);
+ method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVisibilityPublic();
+ method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVisibilityRestricted(java.util.Set<java.lang.String!>);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolume(int);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolumeHandling(int);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolumeMax(int);
@@ -346,7 +352,7 @@
method @MainThread public void addCallback(androidx.mediarouter.media.MediaRouteSelector, androidx.mediarouter.media.MediaRouter.Callback);
method @MainThread public void addCallback(androidx.mediarouter.media.MediaRouteSelector, androidx.mediarouter.media.MediaRouter.Callback, int);
method @MainThread public void addProvider(androidx.mediarouter.media.MediaRouteProvider);
- method @MainThread public void addRemoteControlClient(Object);
+ method @Deprecated @MainThread public void addRemoteControlClient(Object);
method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo? getBluetoothRoute();
method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo getDefaultRoute();
method @MainThread public static androidx.mediarouter.media.MediaRouter getInstance(android.content.Context);
@@ -363,6 +369,7 @@
method @MainThread public void setMediaSession(Object?);
method @MainThread public void setMediaSessionCompat(android.support.v4.media.session.MediaSessionCompat?);
method @MainThread public void setOnPrepareTransferListener(androidx.mediarouter.media.MediaRouter.OnPrepareTransferListener?);
+ method @MainThread public void setRouteListingPreference(androidx.mediarouter.media.RouteListingPreference?);
method @MainThread public void setRouterParams(androidx.mediarouter.media.MediaRouterParams?);
method @MainThread public void unselect(int);
method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo updateSelectedRoute(androidx.mediarouter.media.MediaRouteSelector);
@@ -554,5 +561,53 @@
method public void onSessionStatusChanged(android.os.Bundle?, String, androidx.mediarouter.media.MediaSessionStatus?);
}
+ public final class RouteListingPreference {
+ method public java.util.List<androidx.mediarouter.media.RouteListingPreference.Item!> getItems();
+ method public android.content.ComponentName? getLinkedItemComponentName();
+ method public boolean getUseSystemOrdering();
+ field public static final String ACTION_TRANSFER_MEDIA = "android.media.action.TRANSFER_MEDIA";
+ field public static final String EXTRA_ROUTE_ID = "android.media.extra.ROUTE_ID";
+ }
+
+ public static final class RouteListingPreference.Builder {
+ ctor public RouteListingPreference.Builder();
+ method public androidx.mediarouter.media.RouteListingPreference build();
+ method public androidx.mediarouter.media.RouteListingPreference.Builder setItems(java.util.List<androidx.mediarouter.media.RouteListingPreference.Item!>);
+ method public androidx.mediarouter.media.RouteListingPreference.Builder setLinkedItemComponentName(android.content.ComponentName?);
+ method public androidx.mediarouter.media.RouteListingPreference.Builder setUseSystemOrdering(boolean);
+ }
+
+ public static final class RouteListingPreference.Item {
+ method public CharSequence? getCustomSubtextMessage();
+ method public int getFlags();
+ method public String getRouteId();
+ method public int getSelectionBehavior();
+ method public int getSubText();
+ field public static final int FLAG_ONGOING_SESSION = 1; // 0x1
+ field public static final int FLAG_ONGOING_SESSION_MANAGED = 2; // 0x2
+ field public static final int FLAG_SUGGESTED = 4; // 0x4
+ field public static final int SELECTION_BEHAVIOR_GO_TO_APP = 2; // 0x2
+ field public static final int SELECTION_BEHAVIOR_NONE = 0; // 0x0
+ field public static final int SELECTION_BEHAVIOR_TRANSFER = 1; // 0x1
+ field public static final int SUBTEXT_AD_ROUTING_DISALLOWED = 4; // 0x4
+ field public static final int SUBTEXT_CUSTOM = 10000; // 0x2710
+ field public static final int SUBTEXT_DEVICE_LOW_POWER = 5; // 0x5
+ field public static final int SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED = 3; // 0x3
+ field public static final int SUBTEXT_ERROR_UNKNOWN = 1; // 0x1
+ field public static final int SUBTEXT_NONE = 0; // 0x0
+ field public static final int SUBTEXT_SUBSCRIPTION_REQUIRED = 2; // 0x2
+ field public static final int SUBTEXT_TRACK_UNSUPPORTED = 7; // 0x7
+ field public static final int SUBTEXT_UNAUTHORIZED = 6; // 0x6
+ }
+
+ public static final class RouteListingPreference.Item.Builder {
+ ctor public RouteListingPreference.Item.Builder(String);
+ method public androidx.mediarouter.media.RouteListingPreference.Item build();
+ method public androidx.mediarouter.media.RouteListingPreference.Item.Builder setCustomSubtextMessage(CharSequence?);
+ method public androidx.mediarouter.media.RouteListingPreference.Item.Builder setFlags(int);
+ method public androidx.mediarouter.media.RouteListingPreference.Item.Builder setSelectionBehavior(int);
+ method public androidx.mediarouter.media.RouteListingPreference.Item.Builder setSubText(int);
+ }
+
}
diff --git a/mediarouter/mediarouter/api/public_plus_experimental_current.txt b/mediarouter/mediarouter/api/public_plus_experimental_current.txt
index 8446474..f29ae85 100644
--- a/mediarouter/mediarouter/api/public_plus_experimental_current.txt
+++ b/mediarouter/mediarouter/api/public_plus_experimental_current.txt
@@ -170,8 +170,10 @@
method public android.os.Bundle asBundle();
method public boolean canDisconnectAndKeepPlaying();
method public static androidx.mediarouter.media.MediaRouteDescriptor? fromBundle(android.os.Bundle?);
+ method public java.util.Set<java.lang.String!> getAllowedPackages();
method public int getConnectionState();
method public java.util.List<android.content.IntentFilter!> getControlFilters();
+ method public java.util.Set<java.lang.String!> getDeduplicationIds();
method public String? getDescription();
method public int getDeviceType();
method public android.os.Bundle? getExtras();
@@ -189,6 +191,7 @@
method public boolean isDynamicGroupRoute();
method public boolean isEnabled();
method public boolean isValid();
+ method public boolean isVisibilityPublic();
}
public static final class MediaRouteDescriptor.Builder {
@@ -201,6 +204,7 @@
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setCanDisconnect(boolean);
method @Deprecated public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnecting(boolean);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnectionState(int);
+ method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDeduplicationIds(java.util.Set<java.lang.String!>);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDescription(String?);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDeviceType(int);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setEnabled(boolean);
@@ -213,6 +217,8 @@
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setPlaybackType(int);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setPresentationDisplayId(int);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setSettingsActivity(android.content.IntentSender?);
+ method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVisibilityPublic();
+ method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVisibilityRestricted(java.util.Set<java.lang.String!>);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolume(int);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolumeHandling(int);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolumeMax(int);
@@ -346,7 +352,7 @@
method @MainThread public void addCallback(androidx.mediarouter.media.MediaRouteSelector, androidx.mediarouter.media.MediaRouter.Callback);
method @MainThread public void addCallback(androidx.mediarouter.media.MediaRouteSelector, androidx.mediarouter.media.MediaRouter.Callback, int);
method @MainThread public void addProvider(androidx.mediarouter.media.MediaRouteProvider);
- method @MainThread public void addRemoteControlClient(Object);
+ method @Deprecated @MainThread public void addRemoteControlClient(Object);
method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo? getBluetoothRoute();
method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo getDefaultRoute();
method @MainThread public static androidx.mediarouter.media.MediaRouter getInstance(android.content.Context);
@@ -363,6 +369,7 @@
method @MainThread public void setMediaSession(Object?);
method @MainThread public void setMediaSessionCompat(android.support.v4.media.session.MediaSessionCompat?);
method @MainThread public void setOnPrepareTransferListener(androidx.mediarouter.media.MediaRouter.OnPrepareTransferListener?);
+ method @MainThread public void setRouteListingPreference(androidx.mediarouter.media.RouteListingPreference?);
method @MainThread public void setRouterParams(androidx.mediarouter.media.MediaRouterParams?);
method @MainThread public void unselect(int);
method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo updateSelectedRoute(androidx.mediarouter.media.MediaRouteSelector);
@@ -554,5 +561,53 @@
method public void onSessionStatusChanged(android.os.Bundle?, String, androidx.mediarouter.media.MediaSessionStatus?);
}
+ public final class RouteListingPreference {
+ method public java.util.List<androidx.mediarouter.media.RouteListingPreference.Item!> getItems();
+ method public android.content.ComponentName? getLinkedItemComponentName();
+ method public boolean getUseSystemOrdering();
+ field public static final String ACTION_TRANSFER_MEDIA = "android.media.action.TRANSFER_MEDIA";
+ field public static final String EXTRA_ROUTE_ID = "android.media.extra.ROUTE_ID";
+ }
+
+ public static final class RouteListingPreference.Builder {
+ ctor public RouteListingPreference.Builder();
+ method public androidx.mediarouter.media.RouteListingPreference build();
+ method public androidx.mediarouter.media.RouteListingPreference.Builder setItems(java.util.List<androidx.mediarouter.media.RouteListingPreference.Item!>);
+ method public androidx.mediarouter.media.RouteListingPreference.Builder setLinkedItemComponentName(android.content.ComponentName?);
+ method public androidx.mediarouter.media.RouteListingPreference.Builder setUseSystemOrdering(boolean);
+ }
+
+ public static final class RouteListingPreference.Item {
+ method public CharSequence? getCustomSubtextMessage();
+ method public int getFlags();
+ method public String getRouteId();
+ method public int getSelectionBehavior();
+ method public int getSubText();
+ field public static final int FLAG_ONGOING_SESSION = 1; // 0x1
+ field public static final int FLAG_ONGOING_SESSION_MANAGED = 2; // 0x2
+ field public static final int FLAG_SUGGESTED = 4; // 0x4
+ field public static final int SELECTION_BEHAVIOR_GO_TO_APP = 2; // 0x2
+ field public static final int SELECTION_BEHAVIOR_NONE = 0; // 0x0
+ field public static final int SELECTION_BEHAVIOR_TRANSFER = 1; // 0x1
+ field public static final int SUBTEXT_AD_ROUTING_DISALLOWED = 4; // 0x4
+ field public static final int SUBTEXT_CUSTOM = 10000; // 0x2710
+ field public static final int SUBTEXT_DEVICE_LOW_POWER = 5; // 0x5
+ field public static final int SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED = 3; // 0x3
+ field public static final int SUBTEXT_ERROR_UNKNOWN = 1; // 0x1
+ field public static final int SUBTEXT_NONE = 0; // 0x0
+ field public static final int SUBTEXT_SUBSCRIPTION_REQUIRED = 2; // 0x2
+ field public static final int SUBTEXT_TRACK_UNSUPPORTED = 7; // 0x7
+ field public static final int SUBTEXT_UNAUTHORIZED = 6; // 0x6
+ }
+
+ public static final class RouteListingPreference.Item.Builder {
+ ctor public RouteListingPreference.Item.Builder(String);
+ method public androidx.mediarouter.media.RouteListingPreference.Item build();
+ method public androidx.mediarouter.media.RouteListingPreference.Item.Builder setCustomSubtextMessage(CharSequence?);
+ method public androidx.mediarouter.media.RouteListingPreference.Item.Builder setFlags(int);
+ method public androidx.mediarouter.media.RouteListingPreference.Item.Builder setSelectionBehavior(int);
+ method public androidx.mediarouter.media.RouteListingPreference.Item.Builder setSubText(int);
+ }
+
}
diff --git a/mediarouter/mediarouter/api/restricted_current.txt b/mediarouter/mediarouter/api/restricted_current.txt
index 8446474..f29ae85 100644
--- a/mediarouter/mediarouter/api/restricted_current.txt
+++ b/mediarouter/mediarouter/api/restricted_current.txt
@@ -170,8 +170,10 @@
method public android.os.Bundle asBundle();
method public boolean canDisconnectAndKeepPlaying();
method public static androidx.mediarouter.media.MediaRouteDescriptor? fromBundle(android.os.Bundle?);
+ method public java.util.Set<java.lang.String!> getAllowedPackages();
method public int getConnectionState();
method public java.util.List<android.content.IntentFilter!> getControlFilters();
+ method public java.util.Set<java.lang.String!> getDeduplicationIds();
method public String? getDescription();
method public int getDeviceType();
method public android.os.Bundle? getExtras();
@@ -189,6 +191,7 @@
method public boolean isDynamicGroupRoute();
method public boolean isEnabled();
method public boolean isValid();
+ method public boolean isVisibilityPublic();
}
public static final class MediaRouteDescriptor.Builder {
@@ -201,6 +204,7 @@
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setCanDisconnect(boolean);
method @Deprecated public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnecting(boolean);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnectionState(int);
+ method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDeduplicationIds(java.util.Set<java.lang.String!>);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDescription(String?);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDeviceType(int);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setEnabled(boolean);
@@ -213,6 +217,8 @@
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setPlaybackType(int);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setPresentationDisplayId(int);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setSettingsActivity(android.content.IntentSender?);
+ method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVisibilityPublic();
+ method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVisibilityRestricted(java.util.Set<java.lang.String!>);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolume(int);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolumeHandling(int);
method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setVolumeMax(int);
@@ -346,7 +352,7 @@
method @MainThread public void addCallback(androidx.mediarouter.media.MediaRouteSelector, androidx.mediarouter.media.MediaRouter.Callback);
method @MainThread public void addCallback(androidx.mediarouter.media.MediaRouteSelector, androidx.mediarouter.media.MediaRouter.Callback, int);
method @MainThread public void addProvider(androidx.mediarouter.media.MediaRouteProvider);
- method @MainThread public void addRemoteControlClient(Object);
+ method @Deprecated @MainThread public void addRemoteControlClient(Object);
method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo? getBluetoothRoute();
method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo getDefaultRoute();
method @MainThread public static androidx.mediarouter.media.MediaRouter getInstance(android.content.Context);
@@ -363,6 +369,7 @@
method @MainThread public void setMediaSession(Object?);
method @MainThread public void setMediaSessionCompat(android.support.v4.media.session.MediaSessionCompat?);
method @MainThread public void setOnPrepareTransferListener(androidx.mediarouter.media.MediaRouter.OnPrepareTransferListener?);
+ method @MainThread public void setRouteListingPreference(androidx.mediarouter.media.RouteListingPreference?);
method @MainThread public void setRouterParams(androidx.mediarouter.media.MediaRouterParams?);
method @MainThread public void unselect(int);
method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo updateSelectedRoute(androidx.mediarouter.media.MediaRouteSelector);
@@ -554,5 +561,53 @@
method public void onSessionStatusChanged(android.os.Bundle?, String, androidx.mediarouter.media.MediaSessionStatus?);
}
+ public final class RouteListingPreference {
+ method public java.util.List<androidx.mediarouter.media.RouteListingPreference.Item!> getItems();
+ method public android.content.ComponentName? getLinkedItemComponentName();
+ method public boolean getUseSystemOrdering();
+ field public static final String ACTION_TRANSFER_MEDIA = "android.media.action.TRANSFER_MEDIA";
+ field public static final String EXTRA_ROUTE_ID = "android.media.extra.ROUTE_ID";
+ }
+
+ public static final class RouteListingPreference.Builder {
+ ctor public RouteListingPreference.Builder();
+ method public androidx.mediarouter.media.RouteListingPreference build();
+ method public androidx.mediarouter.media.RouteListingPreference.Builder setItems(java.util.List<androidx.mediarouter.media.RouteListingPreference.Item!>);
+ method public androidx.mediarouter.media.RouteListingPreference.Builder setLinkedItemComponentName(android.content.ComponentName?);
+ method public androidx.mediarouter.media.RouteListingPreference.Builder setUseSystemOrdering(boolean);
+ }
+
+ public static final class RouteListingPreference.Item {
+ method public CharSequence? getCustomSubtextMessage();
+ method public int getFlags();
+ method public String getRouteId();
+ method public int getSelectionBehavior();
+ method public int getSubText();
+ field public static final int FLAG_ONGOING_SESSION = 1; // 0x1
+ field public static final int FLAG_ONGOING_SESSION_MANAGED = 2; // 0x2
+ field public static final int FLAG_SUGGESTED = 4; // 0x4
+ field public static final int SELECTION_BEHAVIOR_GO_TO_APP = 2; // 0x2
+ field public static final int SELECTION_BEHAVIOR_NONE = 0; // 0x0
+ field public static final int SELECTION_BEHAVIOR_TRANSFER = 1; // 0x1
+ field public static final int SUBTEXT_AD_ROUTING_DISALLOWED = 4; // 0x4
+ field public static final int SUBTEXT_CUSTOM = 10000; // 0x2710
+ field public static final int SUBTEXT_DEVICE_LOW_POWER = 5; // 0x5
+ field public static final int SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED = 3; // 0x3
+ field public static final int SUBTEXT_ERROR_UNKNOWN = 1; // 0x1
+ field public static final int SUBTEXT_NONE = 0; // 0x0
+ field public static final int SUBTEXT_SUBSCRIPTION_REQUIRED = 2; // 0x2
+ field public static final int SUBTEXT_TRACK_UNSUPPORTED = 7; // 0x7
+ field public static final int SUBTEXT_UNAUTHORIZED = 6; // 0x6
+ }
+
+ public static final class RouteListingPreference.Item.Builder {
+ ctor public RouteListingPreference.Item.Builder(String);
+ method public androidx.mediarouter.media.RouteListingPreference.Item build();
+ method public androidx.mediarouter.media.RouteListingPreference.Item.Builder setCustomSubtextMessage(CharSequence?);
+ method public androidx.mediarouter.media.RouteListingPreference.Item.Builder setFlags(int);
+ method public androidx.mediarouter.media.RouteListingPreference.Item.Builder setSelectionBehavior(int);
+ method public androidx.mediarouter.media.RouteListingPreference.Item.Builder setSubText(int);
+ }
+
}
diff --git a/mediarouter/mediarouter/build.gradle b/mediarouter/mediarouter/build.gradle
index 2cb2454..3bf9eec 100644
--- a/mediarouter/mediarouter/build.gradle
+++ b/mediarouter/mediarouter/build.gradle
@@ -25,11 +25,12 @@
api("androidx.media:media:1.4.1")
api(libs.guavaListenableFuture)
- implementation("androidx.core:core:1.6.0")
+ implementation("androidx.core:core:1.8.0")
implementation("androidx.appcompat:appcompat:1.1.0")
implementation("androidx.palette:palette:1.0.0")
implementation("androidx.recyclerview:recyclerview:1.1.0")
implementation("androidx.appcompat:appcompat-resources:1.2.0")
+ implementation "androidx.annotation:annotation-experimental:1.3.0"
testImplementation(libs.junit)
testImplementation(libs.testCore)
@@ -41,6 +42,7 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.truth)
androidTestImplementation(libs.espressoCore, excludes.espresso)
androidTestImplementation(project(":media:version-compat-tests:lib"))
androidTestImplementation(project(":mediarouter:mediarouter-testing"))
@@ -57,7 +59,7 @@
}
androidx {
- name = "Android MediaRouter Support Library"
+ name = "MediaRouter"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2013"
description = "Android MediaRouter Support Library"
diff --git a/mediarouter/mediarouter/lint-baseline.xml b/mediarouter/mediarouter/lint-baseline.xml
index fd567a4..8a9547e 100644
--- a/mediarouter/mediarouter/lint-baseline.xml
+++ b/mediarouter/mediarouter/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-beta03" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-beta03)" variant="all" version="8.0.0-beta03">
+<issues format="6" by="lint 8.1.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta01)" variant="all" version="8.1.0-beta01">
<issue
id="NewApi"
@@ -20,6 +20,51 @@
</issue>
<issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" ProviderInfo(MediaRouteProvider provider, boolean treatRouteDescriptorIdsAsUnique) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/mediarouter/media/MediaRouter.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" android.media.RouteListingPreference toPlatformRouteListingPreference() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/mediarouter/media/RouteListingPreference.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" public @interface SelectionBehavior {}"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/mediarouter/media/RouteListingPreference.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" public @interface Flags {}"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/mediarouter/media/RouteListingPreference.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" public @interface SubText {}"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/mediarouter/media/RouteListingPreference.java"/>
+ </issue>
+
+ <issue
id="BanThreadSleep"
message="Uses Thread.sleep()"
errorLine1=" Thread.sleep(TIME_OUT_MS);"
@@ -91,4 +136,40 @@
file="src/androidTest/java/androidx/mediarouter/media/MediaRouterTest.java"/>
</issue>
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (mMr2Provider != null && BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/mediarouter/media/MediaRouter.java"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java"/>
+ </issue>
+
</issues>
diff --git a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouteDescriptorTest.java b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouteDescriptorTest.java
index 504caff..0d3b08c 100644
--- a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouteDescriptorTest.java
+++ b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouteDescriptorTest.java
@@ -17,9 +17,12 @@
package androidx.mediarouter.media;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import android.content.IntentFilter;
+import android.os.Bundle;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -28,7 +31,9 @@
import org.junit.runner.RunWith;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
/**
* Test for {@link MediaRouteDescriptor}.
@@ -43,6 +48,7 @@
private static final String FAKE_MEDIA_ROUTE_ID_4 = "fakeMediaRouteId4";
private static final String FAKE_CONTROL_ACTION_1 = "fakeControlAction1";
private static final String FAKE_CONTROL_ACTION_2 = "fakeControlAction2";
+ private static final String FAKE_PACKAGE_NAME = "com.sample.example";
@Test
@SmallTest
@@ -102,4 +108,114 @@
final List<IntentFilter> controlFilters2 = routeDescriptor.getControlFilters();
assertTrue(controlFilters2.isEmpty());
}
+
+ @Test
+ @SmallTest
+ public void testDefaultVisibilityIsPublic() {
+ MediaRouteDescriptor routeDescriptor = new MediaRouteDescriptor.Builder(
+ FAKE_MEDIA_ROUTE_ID_1, FAKE_MEDIA_ROUTE_NAME)
+ .build();
+
+ assertTrue(routeDescriptor.isVisibilityPublic());
+ }
+
+ @Test
+ @SmallTest
+ public void testIsVisibilityRestricted() {
+ Set<String> allowedPackages = new HashSet<>();
+ allowedPackages.add(FAKE_PACKAGE_NAME);
+ MediaRouteDescriptor routeDescriptor = new MediaRouteDescriptor.Builder(
+ FAKE_MEDIA_ROUTE_ID_1, FAKE_MEDIA_ROUTE_NAME)
+ .setVisibilityRestricted(allowedPackages)
+ .build();
+
+ assertFalse(routeDescriptor.isVisibilityPublic());
+ }
+
+ @Test
+ @SmallTest
+ public void testGetAllowedPackagesReturnsNewInstance() {
+ Set<String> sampleAllowedPackages = new HashSet<>();
+ sampleAllowedPackages.add(FAKE_PACKAGE_NAME);
+ MediaRouteDescriptor routeDescriptor = new MediaRouteDescriptor.Builder(
+ FAKE_MEDIA_ROUTE_ID_1, FAKE_MEDIA_ROUTE_NAME)
+ .setVisibilityRestricted(sampleAllowedPackages)
+ .build();
+
+ Set<String> allowedPackages = routeDescriptor.getAllowedPackages();
+
+ assertEquals(sampleAllowedPackages, allowedPackages);
+ assertNotSame(sampleAllowedPackages, allowedPackages);
+ }
+
+ @Test
+ @SmallTest
+ public void testGetControlFiltersReturnsNewInstance() {
+ IntentFilter f1 = new IntentFilter();
+ f1.addCategory("com.example.androidx.media.CATEGORY_SAMPLE_ROUTE");
+ f1.addAction("com.example.androidx.media.action.TAKE_SNAPSHOT");
+
+ IntentFilter f2 = new IntentFilter();
+ f2.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ f2.addAction(MediaControlIntent.ACTION_PLAY);
+ f2.addDataScheme("http");
+ f2.addDataScheme("https");
+ f2.addDataScheme("rtsp");
+ f2.addDataScheme("file");
+
+ IntentFilter f3 = new IntentFilter();
+ f3.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ f3.addAction(MediaControlIntent.ACTION_SEEK);
+ f3.addAction(MediaControlIntent.ACTION_GET_STATUS);
+ f3.addAction(MediaControlIntent.ACTION_PAUSE);
+ f3.addAction(MediaControlIntent.ACTION_RESUME);
+ f3.addAction(MediaControlIntent.ACTION_STOP);
+
+ List<IntentFilter> sampleControlFilters = new ArrayList<>();
+ sampleControlFilters.add(f1);
+ sampleControlFilters.add(f2);
+ sampleControlFilters.add(f3);
+
+ MediaRouteDescriptor routeDescriptor = new MediaRouteDescriptor.Builder(
+ FAKE_MEDIA_ROUTE_ID_1, FAKE_MEDIA_ROUTE_NAME)
+ .addControlFilter(f1)
+ .addControlFilter(f2)
+ .addControlFilter(f3)
+ .build();
+
+ List<IntentFilter> controlFilters = routeDescriptor.getControlFilters();
+
+ assertEquals(sampleControlFilters, controlFilters);
+ assertNotSame(sampleControlFilters, controlFilters);
+ }
+
+ @Test
+ @SmallTest
+ public void testGetGroupMemberIdsReturnsNewInstance() {
+ List<String> sampleGroupMemberIds = new ArrayList<>();
+ sampleGroupMemberIds.add(FAKE_MEDIA_ROUTE_ID_2);
+ sampleGroupMemberIds.add(FAKE_MEDIA_ROUTE_ID_3);
+ sampleGroupMemberIds.add(FAKE_MEDIA_ROUTE_ID_4);
+ MediaRouteDescriptor routeDescriptor = new MediaRouteDescriptor.Builder(
+ FAKE_MEDIA_ROUTE_ID_1, FAKE_MEDIA_ROUTE_NAME)
+ .addGroupMemberId(FAKE_MEDIA_ROUTE_ID_2)
+ .addGroupMemberId(FAKE_MEDIA_ROUTE_ID_3)
+ .addGroupMemberId(FAKE_MEDIA_ROUTE_ID_4)
+ .build();
+
+ List<String> groupMemberIds = routeDescriptor.getGroupMemberIds();
+
+ assertEquals(sampleGroupMemberIds, groupMemberIds);
+ assertNotSame(sampleGroupMemberIds, groupMemberIds);
+ }
+
+ @Test
+ @SmallTest
+ public void testConstructorUsingBundleReturnsEmptyCollections() {
+ MediaRouteDescriptor routeDescriptor = new MediaRouteDescriptor(new Bundle());
+
+ assertTrue(routeDescriptor.getAllowedPackages().isEmpty());
+ assertTrue(routeDescriptor.getControlFilters().isEmpty());
+ assertTrue(routeDescriptor.getGroupMemberIds().isEmpty());
+ }
}
diff --git a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouter2UtilsTest.java b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouter2UtilsTest.java
index ae769ed..5c524fd 100644
--- a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouter2UtilsTest.java
+++ b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouter2UtilsTest.java
@@ -16,11 +16,17 @@
package androidx.mediarouter.media;
+import static androidx.mediarouter.media.MediaRouter2Utils.KEY_CONTROL_FILTERS;
+import static androidx.mediarouter.media.MediaRouter2Utils.KEY_DEVICE_TYPE;
+import static androidx.mediarouter.media.MediaRouter2Utils.KEY_EXTRAS;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import android.media.MediaRoute2Info;
import android.os.Build;
+import android.os.Bundle;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
@@ -29,6 +35,9 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.ArrayList;
+import java.util.HashSet;
+
/** Test for {@link MediaRouter2Utils}. */
@SmallTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
@@ -60,4 +69,45 @@
.build();
assertNull(MediaRouter2Utils.toFwkMediaRoute2Info(descriptorWithEmptyName));
}
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+ @Test
+ public void toFwkMediaRoute2Info_withDeduplicationIds() {
+ HashSet<String> dedupIds = new HashSet<>();
+ dedupIds.add("dedup_id1");
+ dedupIds.add("dedup_id2");
+ MediaRouteDescriptor descriptor =
+ new MediaRouteDescriptor.Builder(
+ FAKE_MEDIA_ROUTE_DESCRIPTOR_ID, FAKE_MEDIA_ROUTE_DESCRIPTOR_NAME)
+ .setDeduplicationIds(dedupIds)
+ .build();
+ assertTrue(
+ MediaRouter2Utils.toFwkMediaRoute2Info(descriptor)
+ .getDeduplicationIds()
+ .equals(dedupIds));
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+ @Test
+ public void toMediaRouteDescriptor_withDeduplicationIds() {
+ HashSet<String> dedupIds = new HashSet<>();
+ dedupIds.add("dedup_id1");
+ dedupIds.add("dedup_id2");
+ // Extras needed to make toMediaRouteDescriptor not return null.
+ Bundle extras = new Bundle();
+ extras.putBundle(KEY_EXTRAS, new Bundle());
+ extras.putInt(KEY_DEVICE_TYPE, MediaRouter.RouteInfo.DEVICE_TYPE_UNKNOWN);
+ extras.putParcelableArrayList(KEY_CONTROL_FILTERS, new ArrayList<>());
+ MediaRoute2Info routeInfo =
+ new MediaRoute2Info.Builder(
+ FAKE_MEDIA_ROUTE_DESCRIPTOR_ID, FAKE_MEDIA_ROUTE_DESCRIPTOR_NAME)
+ .addFeature(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK)
+ .setDeduplicationIds(dedupIds)
+ .setExtras(extras)
+ .build();
+ assertTrue(
+ MediaRouter2Utils.toMediaRouteDescriptor(routeInfo)
+ .getDeduplicationIds()
+ .equals(dedupIds));
+ }
}
diff --git a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/RouteListingPreferenceTest.java b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/RouteListingPreferenceTest.java
new file mode 100644
index 0000000..e99c4e9
--- /dev/null
+++ b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/RouteListingPreferenceTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.mediarouter.media;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Build;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class RouteListingPreferenceTest {
+
+ private static final String FAKE_ROUTE_ID = "fake_id";
+ private static final String FAKE_CUSTOM_SUBTEXT = "a custom subtext";
+ private static final ComponentName FAKE_COMPONENT_NAME =
+ new ComponentName(
+ ApplicationProvider.getApplicationContext(), RouteListingPreferenceTest.class);
+
+ private Context mContext;
+ private MediaRouter mMediaRouterUnderTest;
+
+ @Before
+ public void setUp() {
+ mContext = ApplicationProvider.getApplicationContext();
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(() -> mMediaRouterUnderTest = MediaRouter.getInstance(mContext));
+ }
+
+ @After
+ public void tearDown() {
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () ->
+ mMediaRouterUnderTest.setRouteListingPreference(
+ /* routeListingPreference= */ null));
+ }
+
+ @SmallTest
+ @Test
+ public void setRouteListingPreference_onAnyApiLevel_doesNotCrash() {
+ // AndroidX infra runs tests on all API levels with significant usage, hence this test
+ // checks this call does not crash regardless of whether route listing preference symbols
+ // are defined on the current platform level.
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () ->
+ mMediaRouterUnderTest.setRouteListingPreference(
+ new RouteListingPreference.Builder().build()));
+ }
+
+ @SmallTest
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+ public void routeListingPreference_yieldsExpectedPlatformEquivalent() {
+ RouteListingPreference.Item fakeRlpItem =
+ new RouteListingPreference.Item.Builder(FAKE_ROUTE_ID)
+ .setFlags(RouteListingPreference.Item.FLAG_SUGGESTED)
+ .setSubText(RouteListingPreference.Item.SUBTEXT_CUSTOM)
+ .setCustomSubtextMessage(FAKE_CUSTOM_SUBTEXT)
+ .setSelectionBehavior(
+ RouteListingPreference.Item.SELECTION_BEHAVIOR_GO_TO_APP)
+ .build();
+ RouteListingPreference fakeRouteListingPreference =
+ new RouteListingPreference.Builder()
+ .setItems(Collections.singletonList(fakeRlpItem))
+ .setLinkedItemComponentName(FAKE_COMPONENT_NAME)
+ .setUseSystemOrdering(false)
+ .build();
+ android.media.RouteListingPreference platformRlp =
+ fakeRouteListingPreference.toPlatformRouteListingPreference();
+
+ assertThat(platformRlp.getUseSystemOrdering()).isFalse();
+ assertThat(platformRlp.getLinkedItemComponentName()).isEqualTo(FAKE_COMPONENT_NAME);
+
+ List<android.media.RouteListingPreference.Item> platformRlpItems = platformRlp.getItems();
+ assertThat(platformRlpItems).hasSize(1);
+ android.media.RouteListingPreference.Item platformRlpItem = platformRlpItems.get(0);
+ assertThat(platformRlpItem.getRouteId()).isEqualTo(FAKE_ROUTE_ID);
+ assertThat(platformRlpItem.getFlags())
+ .isEqualTo(RouteListingPreference.Item.FLAG_SUGGESTED);
+ assertThat(platformRlpItem.getSelectionBehavior())
+ .isEqualTo(RouteListingPreference.Item.SELECTION_BEHAVIOR_GO_TO_APP);
+ assertThat(platformRlpItem.getSubText())
+ .isEqualTo(android.media.RouteListingPreference.Item.SUBTEXT_CUSTOM);
+ assertThat(platformRlpItem.getCustomSubtextMessage()).isEqualTo(FAKE_CUSTOM_SUBTEXT);
+ }
+}
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 9782159..3e74545 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
@@ -22,10 +22,13 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
+import android.media.MediaRouter2;
import android.os.Build;
import android.provider.Settings;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
import java.util.List;
@@ -77,8 +80,10 @@
public static boolean showDialog(@NonNull Context context) {
boolean result = false;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- result = showDialogForAndroidSAndAbove(context)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ result = showDialogForAndroidUAndAbove(context);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ result = showDialogForAndroidSAndT(context)
// The intent action and related string constants are changed in S,
// however they are not public API yet. Try opening the output switcher with the
// old constants for devices that have prior version of the constants.
@@ -98,7 +103,18 @@
return false;
}
- private static boolean showDialogForAndroidSAndAbove(@NonNull Context context) {
+ private static boolean showDialogForAndroidUAndAbove(@NonNull Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ MediaRouter2 mediaRouter2 = Api30Impl.getInstance(context);
+ if (Build.VERSION.SDK_INT >= 34) {
+ return Api34Impl.showSystemOutputSwitcher(mediaRouter2);
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean showDialogForAndroidSAndT(@NonNull Context context) {
Intent intent = new Intent()
.setAction(OUTPUT_SWITCHER_INTENT_ACTION_ANDROID_S)
.setPackage(PACKAGE_NAME_SYSTEM_UI)
@@ -182,4 +198,28 @@
PackageManager packageManager = context.getPackageManager();
return packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH);
}
+
+ @RequiresApi(30)
+ static class Api30Impl {
+ private Api30Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static MediaRouter2 getInstance(Context context) {
+ return MediaRouter2.getInstance(context);
+ }
+ }
+
+ @RequiresApi(34)
+ static class Api34Impl {
+ private Api34Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static boolean showSystemOutputSwitcher(MediaRouter2 mediaRouter2) {
+ return mediaRouter2.showSystemOutputSwitcher();
+ }
+ }
}
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 e77626e..f41e069 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
@@ -44,9 +44,12 @@
import android.util.Log;
import android.util.SparseArray;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
+import androidx.core.os.BuildCompat;
import androidx.mediarouter.R;
import androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor;
import androidx.mediarouter.media.MediaRouter.ControlRequestCallback;
@@ -72,7 +75,7 @@
final Callback mCallback;
final Map<MediaRouter2.RoutingController, GroupRouteController> mControllerMap =
new ArrayMap<>();
- private final MediaRouter2.RouteCallback mRouteCallback = new RouteCallback();
+ private final MediaRouter2.RouteCallback mRouteCallback;
private final MediaRouter2.TransferCallback mTransferCallback = new TransferCallback();
private final MediaRouter2.ControllerCallback mControllerCallback = new ControllerCallback();
private final Handler mHandler;
@@ -81,6 +84,8 @@
private List<MediaRoute2Info> mRoutes = new ArrayList<>();
private Map<String, String> mRouteIdToOriginalRouteIdMap = new ArrayMap<>();
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @SuppressWarnings({"SyntheticAccessor"})
MediaRoute2Provider(@NonNull Context context, @NonNull Callback callback) {
super(context);
mMediaRouter2 = MediaRouter2.getInstance(context);
@@ -88,6 +93,12 @@
mHandler = new Handler(Looper.getMainLooper());
mHandlerExecutor = mHandler::post;
+
+ if (BuildCompat.isAtLeastU()) {
+ mRouteCallback = new RouteCallbackUpsideDownCake();
+ } else {
+ mRouteCallback = new RouteCallback();
+ }
}
@Override
@@ -353,6 +364,16 @@
return new MediaRouteDiscoveryRequest(selector, request.isActiveScan());
}
+ @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ /* package */ void setRouteListingPreference(
+ @Nullable RouteListingPreference routeListingPreference) {
+ Api34Impl.setPlatformRouteListingPreference(
+ mMediaRouter2,
+ routeListingPreference != null
+ ? routeListingPreference.toPlatformRouteListingPreference()
+ : null);
+ }
+
abstract static class Callback {
public abstract void onSelectRoute(@NonNull String routeDescriptorId,
@MediaRouter.UnselectReason int reason);
@@ -380,6 +401,14 @@
}
}
+ private class RouteCallbackUpsideDownCake extends MediaRouter2.RouteCallback {
+
+ @Override
+ public void onRoutesUpdated(@NonNull List<MediaRoute2Info> routes) {
+ refreshRoutes();
+ }
+ }
+
private class TransferCallback extends MediaRouter2.TransferCallback {
TransferCallback() {}
@@ -695,4 +724,18 @@
}
}
}
+
+ @RequiresApi(34)
+ private static class Api34Impl {
+ private Api34Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static void setPlatformRouteListingPreference(
+ @NonNull MediaRouter2 mediaRouter2,
+ @Nullable android.media.RouteListingPreference routeListingPreference) {
+ mediaRouter2.setRouteListingPreference(routeListingPreference);
+ }
+ }
}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteDescriptor.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteDescriptor.java
index 5976828..a1abac9 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteDescriptor.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteDescriptor.java
@@ -17,8 +17,10 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import android.annotation.SuppressLint;
import android.content.IntentFilter;
import android.content.IntentSender;
+import android.media.RouteDiscoveryPreference;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
@@ -31,7 +33,9 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
/**
* Describes the properties of a route.
@@ -65,10 +69,11 @@
static final String KEY_SETTINGS_INTENT = "settingsIntent";
static final String KEY_MIN_CLIENT_VERSION = "minClientVersion";
static final String KEY_MAX_CLIENT_VERSION = "maxClientVersion";
+ static final String KEY_DEDUPLICATION_IDS = "deduplicationIds";
+ static final String KEY_IS_VISIBILITY_PUBLIC = "isVisibilityPublic";
+ static final String KEY_ALLOWED_PACKAGES = "allowedPackages";
final Bundle mBundle;
- List<String> mGroupMemberIds;
- List<IntentFilter> mControlFilters;
MediaRouteDescriptor(Bundle bundle) {
mBundle = bundle;
@@ -97,17 +102,10 @@
@RestrictTo(LIBRARY)
@NonNull
public List<String> getGroupMemberIds() {
- ensureGroupMemberIds();
- return mGroupMemberIds;
- }
-
- void ensureGroupMemberIds() {
- if (mGroupMemberIds == null) {
- mGroupMemberIds = mBundle.getStringArrayList(KEY_GROUP_MEMBER_IDS);
- if (mGroupMemberIds == null) {
- mGroupMemberIds = Collections.emptyList();
- }
+ if (!mBundle.containsKey(KEY_GROUP_MEMBER_IDS)) {
+ return new ArrayList<>();
}
+ return new ArrayList<>(mBundle.getStringArrayList(KEY_GROUP_MEMBER_IDS));
}
/**
@@ -229,17 +227,10 @@
*/
@NonNull
public List<IntentFilter> getControlFilters() {
- ensureControlFilters();
- return mControlFilters;
- }
-
- void ensureControlFilters() {
- if (mControlFilters == null) {
- mControlFilters = mBundle.getParcelableArrayList(KEY_CONTROL_FILTERS);
- if (mControlFilters == null) {
- mControlFilters = Collections.emptyList();
- }
+ if (!mBundle.containsKey(KEY_CONTROL_FILTERS)) {
+ return new ArrayList<>();
}
+ return new ArrayList<>(mBundle.getParcelableArrayList(KEY_CONTROL_FILTERS));
}
/**
@@ -298,6 +289,20 @@
}
/**
+ * Gets the route's deduplication ids.
+ *
+ * <p>Two routes are considered to come from the same receiver device if any of their respective
+ * deduplication ids match.
+ */
+ @NonNull
+ public Set<String> getDeduplicationIds() {
+ ArrayList<String> deduplicationIds = mBundle.getStringArrayList(KEY_DEDUPLICATION_IDS);
+ return deduplicationIds != null
+ ? Collections.unmodifiableSet(new HashSet<>(deduplicationIds))
+ : Collections.emptySet();
+ }
+
+ /**
* Gets the route's presentation display id, or -1 if none.
*/
public int getPresentationDisplayId() {
@@ -333,13 +338,32 @@
}
/**
+ * Gets whether the route visibility is public or not.
+ */
+ public boolean isVisibilityPublic() {
+ return mBundle.getBoolean(KEY_IS_VISIBILITY_PUBLIC, /* defaultValue= */ true);
+ }
+
+ /**
+ * Gets the set of allowed packages which are able to see the route or an empty set if only
+ * the route provider's package is allowed to see this route. This applies only when
+ * {@link #isVisibilityPublic} returns {@code false}.
+ */
+ @NonNull
+ public Set<String> getAllowedPackages() {
+ if (!mBundle.containsKey(KEY_ALLOWED_PACKAGES)) {
+ return new HashSet<>();
+ }
+ return new HashSet<>(mBundle.getStringArrayList(KEY_ALLOWED_PACKAGES));
+ }
+
+ /**
* Returns true if the route descriptor has all of the required fields.
*/
public boolean isValid() {
- ensureControlFilters();
if (TextUtils.isEmpty(getId())
|| TextUtils.isEmpty(getName())
- || mControlFilters.contains(null)) {
+ || getControlFilters().contains(null)) {
return false;
}
return true;
@@ -368,6 +392,8 @@
+ ", isValid=" + isValid()
+ ", minClientVersion=" + getMinClientVersion()
+ ", maxClientVersion=" + getMaxClientVersion()
+ + ", isVisibilityPublic=" + isVisibilityPublic()
+ + ", allowedPackages=" + Arrays.toString(getAllowedPackages().toArray())
+ " }";
}
@@ -397,8 +423,10 @@
*/
public static final class Builder {
private final Bundle mBundle;
- private ArrayList<String> mGroupMemberIds;
- private ArrayList<IntentFilter> mControlFilters;
+
+ private List<String> mGroupMemberIds = new ArrayList<>();
+ private List<IntentFilter> mControlFilters = new ArrayList<>();
+ private Set<String> mAllowedPackages = new HashSet<>();
/**
* Creates a media route descriptor builder.
@@ -423,13 +451,9 @@
mBundle = new Bundle(descriptor.mBundle);
- if (!descriptor.getGroupMemberIds().isEmpty()) {
- mGroupMemberIds = new ArrayList<String>(descriptor.getGroupMemberIds());
- }
-
- if (!descriptor.getControlFilters().isEmpty()) {
- mControlFilters = new ArrayList<IntentFilter>(descriptor.mControlFilters);
- }
+ mGroupMemberIds = descriptor.getGroupMemberIds();
+ mControlFilters = descriptor.getControlFilters();
+ mAllowedPackages = descriptor.getAllowedPackages();
}
/**
@@ -455,9 +479,7 @@
@RestrictTo(LIBRARY)
@NonNull
public Builder clearGroupMemberIds() {
- if (mGroupMemberIds != null) {
- mGroupMemberIds.clear();
- }
+ mGroupMemberIds.clear();
return this;
}
@@ -475,9 +497,6 @@
throw new IllegalArgumentException("groupMemberId must not be empty");
}
- if (mGroupMemberIds == null) {
- mGroupMemberIds = new ArrayList<>();
- }
if (!mGroupMemberIds.contains(groupMemberId)) {
mGroupMemberIds.add(groupMemberId);
}
@@ -519,10 +538,7 @@
if (TextUtils.isEmpty(memberRouteId)) {
throw new IllegalArgumentException("memberRouteId must not be empty");
}
-
- if (mGroupMemberIds != null) {
- mGroupMemberIds.remove(memberRouteId);
- }
+ mGroupMemberIds.remove(memberRouteId);
return this;
}
@@ -600,6 +616,7 @@
mBundle.putBoolean(IS_DYNAMIC_GROUP_ROUTE, isDynamicGroupRoute);
return this;
}
+
/**
* Sets whether the route is in the process of connecting and is not yet
* ready for use.
@@ -650,9 +667,7 @@
*/
@NonNull
public Builder clearControlFilters() {
- if (mControlFilters != null) {
- mControlFilters.clear();
- }
+ mControlFilters.clear();
return this;
}
@@ -665,9 +680,6 @@
throw new IllegalArgumentException("filter must not be null");
}
- if (mControlFilters == null) {
- mControlFilters = new ArrayList<IntentFilter>();
- }
if (!mControlFilters.contains(filter)) {
mControlFilters.add(filter);
}
@@ -760,6 +772,21 @@
}
/**
+ * Sets the route's deduplication ids.
+ *
+ * <p>Two routes are considered to come from the same receiver device if any of their
+ * respective deduplication ids match.
+ *
+ * @param deduplicationIds A set of strings that uniquely identify the receiver device that
+ * backs this route.
+ */
+ @NonNull
+ public Builder setDeduplicationIds(@NonNull Set<String> deduplicationIds) {
+ mBundle.putStringArrayList(KEY_DEDUPLICATION_IDS, new ArrayList<>(deduplicationIds));
+ return this;
+ }
+
+ /**
* Sets the route's presentation display id, or -1 if none.
*/
@NonNull
@@ -806,16 +833,54 @@
}
/**
+ * Sets the visibility of this route to public.
+ *
+ * <p>By default, unless you call {@link #setVisibilityRestricted}, the new route will be
+ * public.
+ *
+ * <p>Public routes are visible to any application with a matching {@link
+ * RouteDiscoveryPreference#getPreferredFeatures feature}.
+ *
+ * <p>Calls to this method override previous calls to {@link #setVisibilityPublic} and
+ * {@link #setVisibilityRestricted}.
+ */
+ @NonNull
+ @SuppressLint({"MissingGetterMatchingBuilder"})
+ public Builder setVisibilityPublic() {
+ mBundle.putBoolean(KEY_IS_VISIBILITY_PUBLIC, true);
+ mAllowedPackages.clear();
+ return this;
+ }
+
+ /**
+ * Sets the visibility of this route to restricted.
+ *
+ * <p>Routes with restricted visibility are only visible to its publisher application and
+ * applications whose package name is included in the provided {@code allowedPackages} set
+ * with a matching {@link RouteDiscoveryPreference#getPreferredFeatures feature}.
+ *
+ * <p>Calls to this method override previous calls to {@link #setVisibilityPublic} and
+ * {@link #setVisibilityRestricted}.
+ *
+ * @see #setVisibilityPublic
+ * @param allowedPackages set of package names which are allowed to see this route.
+ */
+ @NonNull
+ @SuppressLint({"MissingGetterMatchingBuilder"})
+ public Builder setVisibilityRestricted(@NonNull Set<String> allowedPackages) {
+ mBundle.putBoolean(KEY_IS_VISIBILITY_PUBLIC, false);
+ mAllowedPackages = new HashSet<>(allowedPackages);
+ return this;
+ }
+
+ /**
* Builds the {@link MediaRouteDescriptor media route descriptor}.
*/
@NonNull
public MediaRouteDescriptor build() {
- if (mControlFilters != null) {
- mBundle.putParcelableArrayList(KEY_CONTROL_FILTERS, mControlFilters);
- }
- if (mGroupMemberIds != null) {
- mBundle.putStringArrayList(KEY_GROUP_MEMBER_IDS, mGroupMemberIds);
- }
+ mBundle.putParcelableArrayList(KEY_CONTROL_FILTERS, new ArrayList<>(mControlFilters));
+ mBundle.putStringArrayList(KEY_GROUP_MEMBER_IDS, new ArrayList<>(mGroupMemberIds));
+ mBundle.putStringArrayList(KEY_ALLOWED_PACKAGES, new ArrayList<>(mAllowedPackages));
return new MediaRouteDescriptor(mBundle);
}
}
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 44d918b..aa575786 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
@@ -45,12 +45,14 @@
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.collection.ArrayMap;
import androidx.core.app.ActivityManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.core.hardware.display.DisplayManagerCompat;
+import androidx.core.os.BuildCompat;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Pair;
import androidx.media.VolumeProviderCompat;
@@ -899,19 +901,19 @@
}
/**
- * Adds a remote control client to enable remote control of the volume
- * of the selected route.
- * <p>
- * The remote control client must have previously been registered with
- * the audio manager using the {@link android.media.AudioManager#registerRemoteControlClient
+ * Adds a remote control client to enable remote control of the volume of the selected route.
+ *
+ * <p>The remote control client must have previously been registered with the audio manager
+ * using the {@link android.media.AudioManager#registerRemoteControlClient
* AudioManager.registerRemoteControlClient} method.
- * </p>
*
* <p>Must be called on the main thread.
*
* @param remoteControlClient The {@link android.media.RemoteControlClient} to register.
+ * @deprecated Use {@link #setMediaSessionCompat} instead.
*/
@MainThread
+ @Deprecated
public void addRemoteControlClient(@NonNull Object remoteControlClient) {
if (remoteControlClient == null) {
throw new IllegalArgumentException("remoteControlClient must not be null");
@@ -946,14 +948,8 @@
}
/**
- * Sets the media session to enable remote control of the volume of the
- * selected route. This should be used instead of
- * {@link #addRemoteControlClient} when using media sessions. Set the
- * session to null to clear it.
- *
- * <p>Must be called on the main thread.
- *
- * @param mediaSession The {@link android.media.session.MediaSession} to use.
+ * Equivalent to {@link #setMediaSessionCompat}, except it takes an {@link
+ * android.media.session.MediaSession}.
*/
@MainThread
public void setMediaSession(@Nullable Object mediaSession) {
@@ -965,14 +961,16 @@
}
/**
- * Sets a compat media session to enable remote control of the volume of the
- * selected route. This should be used instead of
- * {@link #addRemoteControlClient} when using {@link MediaSessionCompat}.
- * Set the session to null to clear it.
+ * Associates the provided {@link MediaSessionCompat} to this router.
+ *
+ * <p>Maintains the internal state of the provided session to signal it's linked to the
+ * currently selected route at any given time. This guarantees that the system UI shows the
+ * correct route name when applicable.
*
* <p>Must be called on the main thread.
*
- * @param mediaSession The {@link MediaSessionCompat} to use.
+ * @param mediaSession The {@link MediaSessionCompat} to associate to this media router, or null
+ * to clear the existing association.
*/
@MainThread
public void setMediaSessionCompat(@Nullable MediaSessionCompat mediaSession) {
@@ -1018,6 +1016,41 @@
}
/**
+ * Sets the {@link RouteListingPreference} of the app associated to this media router.
+ *
+ * <p>This method does nothing on devices running API 33 or older.
+ *
+ * <p>Use this method to inform the system UI of the routes that you would like to list for
+ * media routing, via the Output Switcher.
+ *
+ * <p>You should call this method immediately after creating an instance and immediately after
+ * receiving any {@link Callback route list changes} in order to keep the system UI in a
+ * consistent state. You can also call this method at any other point to update the listing
+ * preference dynamically (which reflect in the system's Output Switcher).
+ *
+ * <p>Notes:
+ *
+ * <ul>
+ * <li>You should not include the ids of two or more routes with a match in their {@link
+ * MediaRouteDescriptor#getDeduplicationIds() deduplication ids}. If you do, the system
+ * will deduplicate them using its own criteria.
+ * <li>You can use this method to rank routes in the output switcher, placing the more
+ * important routes first. The system might override the proposed ranking.
+ * <li>You can use this method to change how routes are listed using dynamic criteria. For
+ * example, you can disable routing while an {@link
+ * RouteListingPreference.Item#SUBTEXT_AD_ROUTING_DISALLOWED ad is playing}).
+ * </ul>
+ *
+ * @param routeListingPreference The {@link RouteListingPreference} for the system to use for
+ * route listing. When null, the system uses its default listing criteria.
+ */
+ @MainThread
+ public void setRouteListingPreference(@Nullable RouteListingPreference routeListingPreference) {
+ checkCallingThread();
+ getGlobalRouter().setRouteListingPreference(routeListingPreference);
+ }
+
+ /**
* Throws an {@link IllegalStateException} if the calling thread is not the main thread.
*/
static void checkCallingThread() {
@@ -2123,15 +2156,23 @@
* </p>
*/
public static final class ProviderInfo {
+ // Package private fields to avoid use of a synthetic accessor.
final MediaRouteProvider mProviderInstance;
final List<RouteInfo> mRoutes = new ArrayList<>();
+ final boolean mTreatRouteDescriptorIdsAsUnique;
private final ProviderMetadata mMetadata;
private MediaRouteProviderDescriptor mDescriptor;
ProviderInfo(MediaRouteProvider provider) {
+ this(provider, /* treatRouteDescriptorIdsAsUnique= */ false);
+ }
+
+ /** @hide */
+ ProviderInfo(MediaRouteProvider provider, boolean treatRouteDescriptorIdsAsUnique) {
mProviderInstance = provider;
mMetadata = provider.getMetadata();
+ mTreatRouteDescriptorIdsAsUnique = treatRouteDescriptorIdsAsUnique;
}
/**
@@ -2604,9 +2645,9 @@
updateDiscoveryRequest();
}
});
- addProvider(mSystemProvider);
+ addProvider(mSystemProvider, /* treatRouteDescriptorIdsAsUnique= */ true);
if (mMr2Provider != null) {
- addProvider(mMr2Provider);
+ addProvider(mMr2Provider, /* treatRouteDescriptorIdsAsUnique= */ true);
}
// Start watching for routes published by registered media route
@@ -2623,6 +2664,8 @@
mRegisteredProviderWatcher.stop();
mActiveScanThrottlingHelper.reset();
+ setRouteListingPreference(null);
+
setMediaSessionCompat(null);
for (RemoteControlClientRecord record : mRemoteControlClients) {
record.disconnect();
@@ -2741,7 +2784,7 @@
if (mMr2Provider == null) {
mMr2Provider = new MediaRoute2Provider(
mApplicationContext, new Mr2ProviderCallback());
- addProvider(mMr2Provider);
+ addProvider(mMr2Provider, /* treatRouteDescriptorIdsAsUnique= */ true);
// Make sure mDiscoveryRequestForMr2Provider is updated
updateDiscoveryRequest();
mRegisteredProviderWatcher.rescan();
@@ -2767,6 +2810,14 @@
mCallbackHandler.post(CallbackHandler.MSG_ROUTER_PARAMS_CHANGED, params);
}
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ public void setRouteListingPreference(
+ @Nullable RouteListingPreference routeListingPreference) {
+ if (mMr2Provider != null && BuildCompat.isAtLeastU()) {
+ mMr2Provider.setRouteListingPreference(routeListingPreference);
+ }
+ }
+
@Nullable
List<ProviderInfo> getProviders() {
return mProviders;
@@ -3049,12 +3100,18 @@
MediaRouterParams.ENABLE_GROUP_VOLUME_UX, true);
}
-
@Override
public void addProvider(@NonNull MediaRouteProvider providerInstance) {
+ addProvider(providerInstance, /* treatRouteDescriptorIdsAsUnique= */ false);
+ }
+
+ private void addProvider(
+ @NonNull MediaRouteProvider providerInstance,
+ boolean treatRouteDescriptorIdsAsUnique) {
if (findProviderInfo(providerInstance) == null) {
// 1. Add the provider to the list.
- ProviderInfo provider = new ProviderInfo(providerInstance);
+ ProviderInfo provider =
+ new ProviderInfo(providerInstance, treatRouteDescriptorIdsAsUnique);
mProviders.add(provider);
if (DEBUG) {
Log.d(TAG, "Provider added: " + provider);
@@ -3268,8 +3325,11 @@
// possible for there to be two providers with the same package name.
// Therefore we must dedupe the composite id.
String componentName = provider.getComponentName().flattenToShortString();
- String uniqueId = componentName + ":" + routeDescriptorId;
- if (findRouteByUniqueId(uniqueId) < 0) {
+ String uniqueId =
+ provider.mTreatRouteDescriptorIdsAsUnique
+ ? routeDescriptorId
+ : (componentName + ":" + routeDescriptorId);
+ if (provider.mTreatRouteDescriptorIdsAsUnique || findRouteByUniqueId(uniqueId) < 0) {
mUniqueIdMap.put(new Pair<>(componentName, routeDescriptorId), uniqueId);
return uniqueId;
}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java
index 5514799..35a21c1 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java
@@ -35,9 +35,12 @@
import android.text.TextUtils;
import android.util.ArraySet;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
+import androidx.core.os.BuildCompat;
import java.util.ArrayList;
import java.util.Collection;
@@ -65,6 +68,7 @@
private MediaRouter2Utils() {}
+ @OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
@Nullable
public static MediaRoute2Info toFwkMediaRoute2Info(@Nullable MediaRouteDescriptor descriptor) {
if (descriptor == null) {
@@ -88,6 +92,11 @@
//.setClientPackageName(clientMap.get(device.getDeviceId()))
;
+ if (BuildCompat.isAtLeastU()) {
+ Api34Impl.setDeduplicationIds(builder, descriptor.getDeduplicationIds());
+ Api34Impl.copyDescriptorVisibilityToBuilder(builder, descriptor);
+ }
+
switch (descriptor.getDeviceType()) {
case DEVICE_TYPE_TV:
builder.addFeature(FEATURE_REMOTE_VIDEO_PLAYBACK);
@@ -118,6 +127,7 @@
return builder.build();
}
+ @OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
@Nullable
public static MediaRouteDescriptor toMediaRouteDescriptor(
@Nullable MediaRoute2Info fwkMediaRoute2Info) {
@@ -135,6 +145,10 @@
.setEnabled(true)
.setCanDisconnect(false);
+ if (BuildCompat.isAtLeastU()) {
+ builder.setDeduplicationIds(Api34Impl.getDeduplicationIds(fwkMediaRoute2Info));
+ }
+
CharSequence description = fwkMediaRoute2Info.getDescription();
if (description != null) {
builder.setDescription(description.toString());
@@ -276,4 +290,29 @@
}
return routeFeature;
}
+
+ @RequiresApi(api = 34)
+ private static final class Api34Impl {
+
+ @DoNotInline
+ public static void setDeduplicationIds(
+ MediaRoute2Info.Builder builder, Set<String> deduplicationIds) {
+ builder.setDeduplicationIds(deduplicationIds);
+ }
+
+ @DoNotInline
+ public static Set<String> getDeduplicationIds(MediaRoute2Info fwkMediaRoute2Info) {
+ return fwkMediaRoute2Info.getDeduplicationIds();
+ }
+
+ @DoNotInline
+ public static void copyDescriptorVisibilityToBuilder(MediaRoute2Info.Builder builder,
+ MediaRouteDescriptor descriptor) {
+ if (descriptor.isVisibilityPublic()) {
+ builder.setVisibilityPublic();
+ } else {
+ builder.setVisibilityRestricted(descriptor.getAllowedPackages());
+ }
+ }
+ }
}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RouteListingPreference.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RouteListingPreference.java
new file mode 100644
index 0000000..3be72e6
--- /dev/null
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RouteListingPreference.java
@@ -0,0 +1,594 @@
+/*
+ * 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.mediarouter.media;
+
+import android.annotation.SuppressLint;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.text.TextUtils;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * Allows applications to customize the list of routes used for media routing (for example, in the
+ * System UI Output Switcher).
+ *
+ * @see MediaRouter#setRouteListingPreference
+ * @see RouteListingPreference.Item
+ */
+public final class RouteListingPreference {
+
+ /**
+ * {@link Intent} action that the system uses to take the user the app when the user selects an
+ * {@link RouteListingPreference.Item} whose {@link
+ * RouteListingPreference.Item#getSelectionBehavior() selection behavior} is {@link
+ * RouteListingPreference.Item#SELECTION_BEHAVIOR_GO_TO_APP}.
+ *
+ * <p>The launched intent will identify the selected item using the extra identified by {@link
+ * #EXTRA_ROUTE_ID}.
+ *
+ * @see #getLinkedItemComponentName()
+ * @see RouteListingPreference.Item#SELECTION_BEHAVIOR_GO_TO_APP
+ */
+ @SuppressLint("ActionValue") // Field & value copied from android.media.RouteListingPreference.
+ public static final String ACTION_TRANSFER_MEDIA =
+ android.media.RouteListingPreference.ACTION_TRANSFER_MEDIA;
+
+ /**
+ * {@link Intent} string extra key that contains the {@link
+ * RouteListingPreference.Item#getRouteId() id} of the route to transfer to, as part of an
+ * {@link #ACTION_TRANSFER_MEDIA} intent.
+ *
+ * @see #getLinkedItemComponentName()
+ * @see RouteListingPreference.Item#SELECTION_BEHAVIOR_GO_TO_APP
+ */
+ @SuppressLint("ActionValue") // Field & value copied from android.media.RouteListingPreference.
+ public static final String EXTRA_ROUTE_ID = android.media.RouteListingPreference.EXTRA_ROUTE_ID;
+
+ @NonNull private final List<RouteListingPreference.Item> mItems;
+ private final boolean mUseSystemOrdering;
+ @Nullable private final ComponentName mLinkedItemComponentName;
+
+ // Must be package private to avoid a synthetic accessor for the builder.
+ /* package */ RouteListingPreference(RouteListingPreference.Builder builder) {
+ mItems = builder.mItems;
+ mUseSystemOrdering = builder.mUseSystemOrdering;
+ mLinkedItemComponentName = builder.mLinkedItemComponentName;
+ }
+
+ /**
+ * Returns an unmodifiable list containing the {@link RouteListingPreference.Item items} that
+ * the app wants to be listed for media routing.
+ */
+ @NonNull
+ public List<RouteListingPreference.Item> getItems() {
+ return mItems;
+ }
+
+ /**
+ * Returns true if the application would like media route listing to use the system's ordering
+ * strategy, or false if the application would like route listing to respect the ordering
+ * obtained from {@link #getItems()}.
+ *
+ * <p>The system's ordering strategy is implementation-dependent, but may take into account each
+ * route's recency or frequency of use in order to rank them.
+ */
+ public boolean getUseSystemOrdering() {
+ return mUseSystemOrdering;
+ }
+
+ /**
+ * Returns a {@link ComponentName} for navigating to the application.
+ *
+ * <p>Must not be null if any of the {@link #getItems() items} of this route listing preference
+ * has {@link RouteListingPreference.Item#getSelectionBehavior() selection behavior} {@link
+ * RouteListingPreference.Item#SELECTION_BEHAVIOR_GO_TO_APP}.
+ *
+ * <p>The system navigates to the application when the user selects {@link
+ * RouteListingPreference.Item} with {@link
+ * RouteListingPreference.Item#SELECTION_BEHAVIOR_GO_TO_APP} by launching an intent to the
+ * returned {@link ComponentName}, using action {@link #ACTION_TRANSFER_MEDIA}, with the extra
+ * {@link #EXTRA_ROUTE_ID}.
+ */
+ @Nullable
+ public ComponentName getLinkedItemComponentName() {
+ return mLinkedItemComponentName;
+ }
+
+ // Equals and hashCode.
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof RouteListingPreference)) {
+ return false;
+ }
+ RouteListingPreference that = (RouteListingPreference) other;
+ return mItems.equals(that.mItems)
+ && mUseSystemOrdering == that.mUseSystemOrdering
+ && Objects.equals(mLinkedItemComponentName, that.mLinkedItemComponentName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mItems, mUseSystemOrdering, mLinkedItemComponentName);
+ }
+
+ // Internal methods.
+
+ /** @hide */
+ @RequiresApi(api = 34)
+ @NonNull /* package */
+ android.media.RouteListingPreference toPlatformRouteListingPreference() {
+ return Api34Impl.toPlatformRouteListingPreference(this);
+ }
+
+ // Inner classes.
+
+ /** Builder for {@link RouteListingPreference}. */
+ public static final class Builder {
+
+ // The builder fields must be package private to avoid synthetic accessors.
+ /* package */ List<RouteListingPreference.Item> mItems;
+ /* package */ boolean mUseSystemOrdering;
+ /* package */ ComponentName mLinkedItemComponentName;
+
+ /** Creates a new instance with default values (documented in the setters). */
+ public Builder() {
+ mItems = Collections.emptyList();
+ mUseSystemOrdering = true;
+ }
+
+ /**
+ * See {@link #getItems()}
+ *
+ * <p>The default value is an empty list.
+ */
+ @NonNull
+ public RouteListingPreference.Builder setItems(
+ @NonNull List<RouteListingPreference.Item> items) {
+ mItems = Collections.unmodifiableList(new ArrayList<>(Objects.requireNonNull(items)));
+ return this;
+ }
+
+ /**
+ * See {@link #getUseSystemOrdering()}
+ *
+ * <p>The default value is {@code true}.
+ */
+ // Lint requires "isUseSystemOrdering", but "getUseSystemOrdering" is a better name.
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ @NonNull
+ public RouteListingPreference.Builder setUseSystemOrdering(boolean useSystemOrdering) {
+ mUseSystemOrdering = useSystemOrdering;
+ return this;
+ }
+
+ /**
+ * See {@link #getLinkedItemComponentName()}.
+ *
+ * <p>The default value is {@code null}.
+ */
+ @NonNull
+ public RouteListingPreference.Builder setLinkedItemComponentName(
+ @Nullable ComponentName linkedItemComponentName) {
+ mLinkedItemComponentName = linkedItemComponentName;
+ return this;
+ }
+
+ /**
+ * Creates and returns a new {@link RouteListingPreference} instance with the given
+ * parameters.
+ */
+ @NonNull
+ public RouteListingPreference build() {
+ return new RouteListingPreference(this);
+ }
+ }
+
+ /** Holds preference information for a specific route in a {@link RouteListingPreference}. */
+ public static final class Item {
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ SELECTION_BEHAVIOR_NONE,
+ SELECTION_BEHAVIOR_TRANSFER,
+ SELECTION_BEHAVIOR_GO_TO_APP
+ })
+ public @interface SelectionBehavior {}
+
+ /** The corresponding route is not selectable by the user. */
+ public static final int SELECTION_BEHAVIOR_NONE = 0;
+ /** If the user selects the corresponding route, the media transfers to the said route. */
+ public static final int SELECTION_BEHAVIOR_TRANSFER = 1;
+ /**
+ * If the user selects the corresponding route, the system takes the user to the
+ * application.
+ *
+ * <p>The system uses {@link #getLinkedItemComponentName()} in order to navigate to the app.
+ */
+ public static final int SELECTION_BEHAVIOR_GO_TO_APP = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {FLAG_ONGOING_SESSION, FLAG_ONGOING_SESSION_MANAGED, FLAG_SUGGESTED})
+ public @interface Flags {}
+
+ /**
+ * The corresponding route is already hosting a session with the app that owns this listing
+ * preference.
+ */
+ public static final int FLAG_ONGOING_SESSION = 1;
+
+ /**
+ * Signals that the ongoing session on the corresponding route is managed by the current
+ * user of the app.
+ *
+ * <p>The system can use this flag to provide visual indication that the route is not only
+ * hosting a session, but also that the user has ownership over said session.
+ *
+ * <p>This flag is ignored if {@link #FLAG_ONGOING_SESSION} is not set, or if the
+ * corresponding route is not currently selected.
+ *
+ * <p>This flag does not affect volume adjustment (see {@link
+ * androidx.media.VolumeProviderCompat}, and {@link
+ * MediaRouteDescriptor#getVolumeHandling()}), or any aspect other than the visual
+ * representation of the corresponding item.
+ */
+ public static final int FLAG_ONGOING_SESSION_MANAGED = 1 << 1;
+
+ /**
+ * The corresponding route is specially likely to be selected by the user.
+ *
+ * <p>A UI reflecting this preference may reserve a specific space for suggested routes,
+ * making it more accessible to the user. If the number of suggested routes exceeds the
+ * number supported by the UI, the routes listed first in {@link
+ * RouteListingPreference#getItems()} will take priority.
+ */
+ public static final int FLAG_SUGGESTED = 1 << 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ SUBTEXT_NONE,
+ SUBTEXT_ERROR_UNKNOWN,
+ SUBTEXT_SUBSCRIPTION_REQUIRED,
+ SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED,
+ SUBTEXT_AD_ROUTING_DISALLOWED,
+ SUBTEXT_DEVICE_LOW_POWER,
+ SUBTEXT_UNAUTHORIZED,
+ SUBTEXT_TRACK_UNSUPPORTED,
+ SUBTEXT_CUSTOM
+ })
+ public @interface SubText {}
+
+ /** The corresponding route has no associated subtext. */
+ public static final int SUBTEXT_NONE =
+ android.media.RouteListingPreference.Item.SUBTEXT_NONE;
+ /**
+ * The corresponding route's subtext must indicate that it is not available because of an
+ * unknown error.
+ */
+ public static final int SUBTEXT_ERROR_UNKNOWN =
+ android.media.RouteListingPreference.Item.SUBTEXT_ERROR_UNKNOWN;
+ /**
+ * The corresponding route's subtext must indicate that it requires a special subscription
+ * in order to be available for routing.
+ */
+ public static final int SUBTEXT_SUBSCRIPTION_REQUIRED =
+ android.media.RouteListingPreference.Item.SUBTEXT_SUBSCRIPTION_REQUIRED;
+ /**
+ * The corresponding route's subtext must indicate that downloaded content cannot be routed
+ * to it.
+ */
+ public static final int SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED =
+ android.media.RouteListingPreference.Item
+ .SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED;
+ /**
+ * The corresponding route's subtext must indicate that it is not available because an ad is
+ * in progress.
+ */
+ public static final int SUBTEXT_AD_ROUTING_DISALLOWED =
+ android.media.RouteListingPreference.Item.SUBTEXT_AD_ROUTING_DISALLOWED;
+ /**
+ * The corresponding route's subtext must indicate that it is not available because the
+ * device is in low-power mode.
+ */
+ public static final int SUBTEXT_DEVICE_LOW_POWER =
+ android.media.RouteListingPreference.Item.SUBTEXT_DEVICE_LOW_POWER;
+ /**
+ * The corresponding route's subtext must indicate that it is not available because the user
+ * is not authorized to route to it.
+ */
+ public static final int SUBTEXT_UNAUTHORIZED =
+ android.media.RouteListingPreference.Item.SUBTEXT_UNAUTHORIZED;
+ /**
+ * The corresponding route's subtext must indicate that it is not available because the
+ * device does not support the current media track.
+ */
+ public static final int SUBTEXT_TRACK_UNSUPPORTED =
+ android.media.RouteListingPreference.Item.SUBTEXT_TRACK_UNSUPPORTED;
+ /**
+ * The corresponding route's subtext must be obtained from {@link
+ * #getCustomSubtextMessage()}.
+ *
+ * <p>Applications should strongly prefer one of the other disable reasons (for the full
+ * list, see {@link #getSubText()}) in order to guarantee correct localization and rendering
+ * across all form factors.
+ */
+ public static final int SUBTEXT_CUSTOM =
+ android.media.RouteListingPreference.Item.SUBTEXT_CUSTOM;
+
+ @NonNull private final String mRouteId;
+ @SelectionBehavior private final int mSelectionBehavior;
+ @Flags private final int mFlags;
+ @SubText private final int mSubText;
+
+ @Nullable private final CharSequence mCustomSubtextMessage;
+
+ // Must be package private to avoid a synthetic accessor for the builder.
+ /* package */ Item(@NonNull RouteListingPreference.Item.Builder builder) {
+ mRouteId = builder.mRouteId;
+ mSelectionBehavior = builder.mSelectionBehavior;
+ mFlags = builder.mFlags;
+ mSubText = builder.mSubText;
+ mCustomSubtextMessage = builder.mCustomSubtextMessage;
+ validateCustomMessageSubtext();
+ }
+
+ /**
+ * Returns the id of the route that corresponds to this route listing preference item.
+ *
+ * @see MediaRouter.RouteInfo#getId()
+ */
+ @NonNull
+ public String getRouteId() {
+ return mRouteId;
+ }
+
+ /**
+ * Returns the behavior that the corresponding route has if the user selects it.
+ *
+ * @see #SELECTION_BEHAVIOR_NONE
+ * @see #SELECTION_BEHAVIOR_TRANSFER
+ * @see #SELECTION_BEHAVIOR_GO_TO_APP
+ */
+ public int getSelectionBehavior() {
+ return mSelectionBehavior;
+ }
+
+ /**
+ * Returns the flags associated to the route that corresponds to this item.
+ *
+ * @see #FLAG_ONGOING_SESSION
+ * @see #FLAG_ONGOING_SESSION_MANAGED
+ * @see #FLAG_SUGGESTED
+ */
+ @Flags
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Returns the type of subtext associated to this route.
+ *
+ * <p>Subtext types other than {@link #SUBTEXT_NONE} and {@link #SUBTEXT_CUSTOM} must not
+ * have {@link #SELECTION_BEHAVIOR_TRANSFER}.
+ *
+ * <p>If this method returns {@link #SUBTEXT_CUSTOM}, then the subtext is obtained form
+ * {@link #getCustomSubtextMessage()}.
+ *
+ * @see #SUBTEXT_NONE
+ * @see #SUBTEXT_ERROR_UNKNOWN
+ * @see #SUBTEXT_SUBSCRIPTION_REQUIRED
+ * @see #SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED
+ * @see #SUBTEXT_AD_ROUTING_DISALLOWED
+ * @see #SUBTEXT_DEVICE_LOW_POWER
+ * @see #SUBTEXT_UNAUTHORIZED
+ * @see #SUBTEXT_TRACK_UNSUPPORTED
+ * @see #SUBTEXT_CUSTOM
+ */
+ @SubText
+ public int getSubText() {
+ return mSubText;
+ }
+
+ /**
+ * Returns a human-readable {@link CharSequence} providing the subtext for the corresponding
+ * route.
+ *
+ * <p>This value is ignored if the {@link #getSubText() subtext} for this item is not {@link
+ * #SUBTEXT_CUSTOM}..
+ *
+ * <p>Applications must provide a localized message that matches the system's locale. See
+ * {@link Locale#getDefault()}.
+ *
+ * <p>Applications should avoid using custom messages (and instead use one of non-custom
+ * subtexts listed in {@link #getSubText()} in order to guarantee correct visual
+ * representation and localization on all form factors.
+ */
+ @Nullable
+ public CharSequence getCustomSubtextMessage() {
+ return mCustomSubtextMessage;
+ }
+
+ // Equals and hashCode.
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof RouteListingPreference.Item)) {
+ return false;
+ }
+ RouteListingPreference.Item item = (RouteListingPreference.Item) other;
+ return mRouteId.equals(item.mRouteId)
+ && mSelectionBehavior == item.mSelectionBehavior
+ && mFlags == item.mFlags
+ && mSubText == item.mSubText
+ && TextUtils.equals(mCustomSubtextMessage, item.mCustomSubtextMessage);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mRouteId, mSelectionBehavior, mFlags, mSubText, mCustomSubtextMessage);
+ }
+
+ // Internal methods.
+
+ private void validateCustomMessageSubtext() {
+ Preconditions.checkArgument(
+ mSubText != SUBTEXT_CUSTOM || mCustomSubtextMessage != null,
+ "The custom subtext message cannot be null if subtext is SUBTEXT_CUSTOM.");
+ }
+
+ // Internal classes.
+
+ /** Builder for {@link RouteListingPreference.Item}. */
+ public static final class Builder {
+
+ // The builder fields must be package private to avoid synthetic accessors.
+ /* package */ final String mRouteId;
+ /* package */ int mSelectionBehavior;
+ /* package */ int mFlags;
+ /* package */ int mSubText;
+ /* package */ CharSequence mCustomSubtextMessage;
+
+ /**
+ * Constructor.
+ *
+ * @param routeId See {@link RouteListingPreference.Item#getRouteId()}.
+ */
+ public Builder(@NonNull String routeId) {
+ Preconditions.checkArgument(!TextUtils.isEmpty(routeId));
+ mRouteId = routeId;
+ mSelectionBehavior = SELECTION_BEHAVIOR_TRANSFER;
+ mSubText = SUBTEXT_NONE;
+ }
+
+ /**
+ * See {@link RouteListingPreference.Item#getSelectionBehavior()}.
+ *
+ * <p>The default value is {@link #ACTION_TRANSFER_MEDIA}.
+ */
+ @NonNull
+ public RouteListingPreference.Item.Builder setSelectionBehavior(int selectionBehavior) {
+ mSelectionBehavior = selectionBehavior;
+ return this;
+ }
+
+ /**
+ * See {@link RouteListingPreference.Item#getFlags()}.
+ *
+ * <p>The default value is zero (no flags).
+ */
+ @NonNull
+ public RouteListingPreference.Item.Builder setFlags(int flags) {
+ mFlags = flags;
+ return this;
+ }
+
+ /**
+ * See {@link RouteListingPreference.Item#getSubText()}.
+ *
+ * <p>The default value is {@link #SUBTEXT_NONE}.
+ */
+ @NonNull
+ public RouteListingPreference.Item.Builder setSubText(int subText) {
+ mSubText = subText;
+ return this;
+ }
+
+ /**
+ * See {@link RouteListingPreference.Item#getCustomSubtextMessage()}.
+ *
+ * <p>The default value is {@code null}.
+ */
+ @NonNull
+ public RouteListingPreference.Item.Builder setCustomSubtextMessage(
+ @Nullable CharSequence customSubtextMessage) {
+ mCustomSubtextMessage = customSubtextMessage;
+ return this;
+ }
+
+ /**
+ * Creates and returns a new {@link RouteListingPreference.Item} with the given
+ * parameters.
+ */
+ @NonNull
+ public RouteListingPreference.Item build() {
+ return new RouteListingPreference.Item(this);
+ }
+ }
+ }
+
+ @RequiresApi(34)
+ private static class Api34Impl {
+ private Api34Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ @NonNull
+ public static android.media.RouteListingPreference toPlatformRouteListingPreference(
+ RouteListingPreference routeListingPreference) {
+ ArrayList<android.media.RouteListingPreference.Item> platformRlpItems =
+ new ArrayList<>();
+ for (Item item : routeListingPreference.getItems()) {
+ platformRlpItems.add(toPlatformItem(item));
+ }
+
+ return new android.media.RouteListingPreference.Builder()
+ .setItems(platformRlpItems)
+ .setLinkedItemComponentName(routeListingPreference.getLinkedItemComponentName())
+ .setUseSystemOrdering(routeListingPreference.getUseSystemOrdering())
+ .build();
+ }
+
+ @DoNotInline
+ @NonNull
+ public static android.media.RouteListingPreference.Item toPlatformItem(Item item) {
+ return new android.media.RouteListingPreference.Item.Builder(item.getRouteId())
+ .setFlags(item.getFlags())
+ .setSubText(item.getSubText())
+ .setCustomSubtextMessage(item.getCustomSubtextMessage())
+ .setSelectionBehavior(item.getSelectionBehavior())
+ .build();
+ }
+ }
+}
diff --git a/metrics/metrics-performance/build.gradle b/metrics/metrics-performance/build.gradle
index 94fcacb..8c474b0 100644
--- a/metrics/metrics-performance/build.gradle
+++ b/metrics/metrics-performance/build.gradle
@@ -59,7 +59,7 @@
androidx {
- name = "AndroidX Metrics"
+ name = "Metrics"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2021"
description = "Library for tracking and reporting various runtime metrics for applications"
diff --git a/navigation/navigation-common-ktx/build.gradle b/navigation/navigation-common-ktx/build.gradle
index 03904c3..b2f94223 100644
--- a/navigation/navigation-common-ktx/build.gradle
+++ b/navigation/navigation-common-ktx/build.gradle
@@ -32,7 +32,7 @@
}
androidx {
- name = "Android Navigation Common Kotlin Extensions"
+ name = "Navigation Common Kotlin Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Android Navigation-Common-Ktx"
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index 954e0da..91a97ad3 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -67,7 +67,7 @@
}
androidx {
- name = "Android Navigation Common"
+ name = "Navigation Common"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Navigation-Common"
diff --git a/navigation/navigation-compose/samples/build.gradle b/navigation/navigation-compose/samples/build.gradle
index b71e29c..b229762 100644
--- a/navigation/navigation-compose/samples/build.gradle
+++ b/navigation/navigation-compose/samples/build.gradle
@@ -39,7 +39,7 @@
}
androidx {
- name = "AndroidX Compose UI Navigation Integration Samples"
+ name = "Compose UI Navigation Integration Samples"
type = LibraryType.SAMPLES
inceptionYear = "2020"
description = "Samples for Compose integration with Navigation"
diff --git a/navigation/navigation-dynamic-features-fragment/build.gradle b/navigation/navigation-dynamic-features-fragment/build.gradle
index 80119e7..931ddb1 100644
--- a/navigation/navigation-dynamic-features-fragment/build.gradle
+++ b/navigation/navigation-dynamic-features-fragment/build.gradle
@@ -64,7 +64,7 @@
}
androidx {
- name = "Android Dynamic Feature Navigation Fragment"
+ name = "Dynamic Feature Navigation Fragment"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
description = "Android Dynamic Feature Navigation Fragment"
diff --git a/navigation/navigation-dynamic-features-runtime/build.gradle b/navigation/navigation-dynamic-features-runtime/build.gradle
index e722e52..497c5ea 100644
--- a/navigation/navigation-dynamic-features-runtime/build.gradle
+++ b/navigation/navigation-dynamic-features-runtime/build.gradle
@@ -60,7 +60,7 @@
}
androidx {
- name = "Android Dynamic Feature Navigation Runtime"
+ name = "Dynamic Feature Navigation Runtime"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
description = "Android Dynamic Feature Navigation Runtime"
diff --git a/navigation/navigation-fragment-ktx/build.gradle b/navigation/navigation-fragment-ktx/build.gradle
index 2cb0659..bd96b86 100644
--- a/navigation/navigation-fragment-ktx/build.gradle
+++ b/navigation/navigation-fragment-ktx/build.gradle
@@ -30,7 +30,7 @@
}
androidx {
- name = "Android Navigation Fragment Kotlin Extensions"
+ name = "Navigation Fragment Kotlin Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Android Navigation-Fragment-Ktx"
diff --git a/navigation/navigation-fragment/build.gradle b/navigation/navigation-fragment/build.gradle
index f8284f5..5edeaf7 100644
--- a/navigation/navigation-fragment/build.gradle
+++ b/navigation/navigation-fragment/build.gradle
@@ -43,7 +43,7 @@
}
androidx {
- name = "Android Navigation Fragment"
+ name = "Navigation Fragment"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Navigation-Fragment"
diff --git a/navigation/navigation-runtime-ktx/build.gradle b/navigation/navigation-runtime-ktx/build.gradle
index 1b497c7..4663086 100644
--- a/navigation/navigation-runtime-ktx/build.gradle
+++ b/navigation/navigation-runtime-ktx/build.gradle
@@ -30,7 +30,7 @@
}
androidx {
- name = "Android Navigation Runtime Kotlin Extensions"
+ name = "Navigation Runtime Kotlin Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Android Navigation-Runtime-Ktx"
diff --git a/navigation/navigation-runtime-truth/build.gradle b/navigation/navigation-runtime-truth/build.gradle
index dfc5054..4ff4554f 100644
--- a/navigation/navigation-runtime-truth/build.gradle
+++ b/navigation/navigation-runtime-truth/build.gradle
@@ -38,7 +38,7 @@
}
androidx {
- name = "Android Navigation Runtime Truth"
+ name = "Navigation Runtime Truth"
publish = Publish.NONE
inceptionYear = "2019"
description = "Android Navigation-Runtime-Truth"
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index c7ebc80..ff856e6 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -60,7 +60,7 @@
}
androidx {
- name = "Android Navigation Runtime"
+ name = "Navigation Runtime"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Navigation-Runtime"
diff --git a/navigation/navigation-safe-args-generator/build.gradle b/navigation/navigation-safe-args-generator/build.gradle
index 4b4c91f..988bc00 100644
--- a/navigation/navigation-safe-args-generator/build.gradle
+++ b/navigation/navigation-safe-args-generator/build.gradle
@@ -60,7 +60,7 @@
androidx {
- name = "Android Navigation TypeSafe Arguments Generator"
+ name = "Navigation TypeSafe Arguments Generator"
type = LibraryType.OTHER_CODE_PROCESSOR
inceptionYear = "2017"
description = "Android Navigation TypeSafe Arguments Generator"
diff --git a/navigation/navigation-safe-args-gradle-plugin/build.gradle b/navigation/navigation-safe-args-gradle-plugin/build.gradle
index 1d740b2..08697dc 100644
--- a/navigation/navigation-safe-args-gradle-plugin/build.gradle
+++ b/navigation/navigation-safe-args-gradle-plugin/build.gradle
@@ -50,7 +50,7 @@
}
androidx {
- name = "Android Navigation TypeSafe Arguments Gradle Plugin"
+ name = "Navigation TypeSafe Arguments Gradle Plugin"
type = LibraryType.GRADLE_PLUGIN
inceptionYear = "2017"
description = "Android Navigation TypeSafe Arguments Gradle Plugin"
diff --git a/navigation/navigation-testing/build.gradle b/navigation/navigation-testing/build.gradle
index d30a3fc..4008f8e 100644
--- a/navigation/navigation-testing/build.gradle
+++ b/navigation/navigation-testing/build.gradle
@@ -38,7 +38,7 @@
}
androidx {
- name = "Android Navigation Testing"
+ name = "Navigation Testing"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Navigation-Testing"
diff --git a/navigation/navigation-ui-ktx/build.gradle b/navigation/navigation-ui-ktx/build.gradle
index 3cbe68b..23f6129 100644
--- a/navigation/navigation-ui-ktx/build.gradle
+++ b/navigation/navigation-ui-ktx/build.gradle
@@ -30,7 +30,7 @@
}
androidx {
- name = "Android Navigation UI Kotlin Extensions"
+ name = "Navigation UI Kotlin Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Android Navigation-UI-Ktx"
diff --git a/navigation/navigation-ui/build.gradle b/navigation/navigation-ui/build.gradle
index 5dd6cca..4877297 100644
--- a/navigation/navigation-ui/build.gradle
+++ b/navigation/navigation-ui/build.gradle
@@ -55,7 +55,7 @@
}
androidx {
- name = "Android Navigation UI"
+ name = "Navigation UI"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Android Navigation-UI"
diff --git a/paging/paging-common-ktx/build.gradle b/paging/paging-common-ktx/build.gradle
index bab41ca..f5a7a7e 100644
--- a/paging/paging-common-ktx/build.gradle
+++ b/paging/paging-common-ktx/build.gradle
@@ -26,7 +26,7 @@
}
androidx {
- name = "Android Paging-Common Kotlin Extensions"
+ name = "Paging-Common Kotlin Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Kotlin extensions for 'paging-common' artifact"
diff --git a/paging/paging-common/build.gradle b/paging/paging-common/build.gradle
index 00ad658..bbc62bf 100644
--- a/paging/paging-common/build.gradle
+++ b/paging/paging-common/build.gradle
@@ -51,7 +51,7 @@
}
androidx {
- name = "Android Paging-Common"
+ name = "Paging-Common"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Paging-Common"
diff --git a/paging/paging-compose/build.gradle b/paging/paging-compose/build.gradle
index d31e914..8affe5a 100644
--- a/paging/paging-compose/build.gradle
+++ b/paging/paging-compose/build.gradle
@@ -43,7 +43,7 @@
}
androidx {
- name = "Android Paging-Compose"
+ name = "Paging-Compose"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.PAGING_COMPOSE
inceptionYear = "2020"
diff --git a/paging/paging-compose/samples/build.gradle b/paging/paging-compose/samples/build.gradle
index 6fd15b4..753063b 100644
--- a/paging/paging-compose/samples/build.gradle
+++ b/paging/paging-compose/samples/build.gradle
@@ -34,7 +34,7 @@
}
androidx {
- name = "AndroidX Paging Compose Samples"
+ name = "Paging Compose Samples"
type = LibraryType.SAMPLES
mavenVersion = LibraryVersions.PAGING_COMPOSE
inceptionYear = "2020"
diff --git a/paging/paging-guava/build.gradle b/paging/paging-guava/build.gradle
index 041702f..f0151c2 100644
--- a/paging/paging-guava/build.gradle
+++ b/paging/paging-guava/build.gradle
@@ -39,7 +39,7 @@
}
androidx {
- name = "Android Paging Guava"
+ name = "Paging Guava"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
description = "Android Paging Guava"
diff --git a/paging/paging-runtime-ktx/build.gradle b/paging/paging-runtime-ktx/build.gradle
index 3f2e05d..f674b7e 100644
--- a/paging/paging-runtime-ktx/build.gradle
+++ b/paging/paging-runtime-ktx/build.gradle
@@ -29,7 +29,7 @@
}
androidx {
- name = "Android Paging-Runtime Kotlin Extensions"
+ name = "Paging-Runtime Kotlin Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Kotlin extensions for 'paging-runtime' artifact"
diff --git a/paging/paging-runtime/build.gradle b/paging/paging-runtime/build.gradle
index 47fdac8..de64d71 100644
--- a/paging/paging-runtime/build.gradle
+++ b/paging/paging-runtime/build.gradle
@@ -61,7 +61,7 @@
}
androidx {
- name = "Android Paging-Runtime"
+ name = "Paging-Runtime"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Paging-Runtime"
diff --git a/paging/paging-rxjava2-ktx/build.gradle b/paging/paging-rxjava2-ktx/build.gradle
index a355219..b53bd80 100644
--- a/paging/paging-rxjava2-ktx/build.gradle
+++ b/paging/paging-rxjava2-ktx/build.gradle
@@ -36,7 +36,7 @@
}
androidx {
- name = "Android Paging RxJava2 Kotlin Extensions"
+ name = "Paging RxJava2 Kotlin Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Kotlin extensions for 'paging-rxjava2' artifact"
diff --git a/paging/paging-rxjava2/build.gradle b/paging/paging-rxjava2/build.gradle
index c830a91..5f779d0 100644
--- a/paging/paging-rxjava2/build.gradle
+++ b/paging/paging-rxjava2/build.gradle
@@ -47,7 +47,7 @@
}
androidx {
- name = "Android Paging-RXJava2"
+ name = "Paging-RXJava2"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Android Paging-RXJava2"
diff --git a/paging/paging-rxjava3/build.gradle b/paging/paging-rxjava3/build.gradle
index dc27213..05d795c 100644
--- a/paging/paging-rxjava3/build.gradle
+++ b/paging/paging-rxjava3/build.gradle
@@ -45,7 +45,7 @@
}
androidx {
- name = "Android Paging-RxJava3"
+ name = "Paging-RxJava3"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2020"
description = "Android Paging-RxJava3"
diff --git a/paging/paging-testing/build.gradle b/paging/paging-testing/build.gradle
index 18e00f6..9557e62 100644
--- a/paging/paging-testing/build.gradle
+++ b/paging/paging-testing/build.gradle
@@ -36,7 +36,7 @@
}
androidx {
- name = "androidx.paging:paging-testing"
+ name = "Paging Testing Extensions"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2022"
description = "Test artifact for Paging implementation"
diff --git a/paging/samples/build.gradle b/paging/samples/build.gradle
index 4a7e36a..9e5516a 100644
--- a/paging/samples/build.gradle
+++ b/paging/samples/build.gradle
@@ -52,7 +52,7 @@
}
androidx {
- name = "AndroidX Paging Samples"
+ name = "Paging Samples"
type = LibraryType.SAMPLES
inceptionYear = "2019"
description = "Contains the sample code for the Androidx Paging library"
diff --git a/palette/palette/build.gradle b/palette/palette/build.gradle
index 90bdee7..df6b451 100644
--- a/palette/palette/build.gradle
+++ b/palette/palette/build.gradle
@@ -17,7 +17,7 @@
}
androidx {
- name = "Android Support Palette"
+ name = "Palette"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2014"
description = "Android Support Palette"
diff --git a/percentlayout/percentlayout/build.gradle b/percentlayout/percentlayout/build.gradle
index a28d3b0..737f4d4 100644
--- a/percentlayout/percentlayout/build.gradle
+++ b/percentlayout/percentlayout/build.gradle
@@ -24,7 +24,7 @@
}
androidx {
- name = "Android Percent Support Library"
+ name = "Percent"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2015"
description = "Android Percent Support Library"
diff --git a/percentlayout/percentlayout/src/androidTest/java/androidx/percentlayout/widget/BaseTestActivity.java b/percentlayout/percentlayout/src/androidTest/java/androidx/percentlayout/widget/BaseTestActivity.java
index 790793b..9dcf237 100755
--- a/percentlayout/percentlayout/src/androidTest/java/androidx/percentlayout/widget/BaseTestActivity.java
+++ b/percentlayout/percentlayout/src/androidTest/java/androidx/percentlayout/widget/BaseTestActivity.java
@@ -22,6 +22,7 @@
abstract class BaseTestActivity extends Activity {
@Override
+ @SuppressWarnings("deprecation")
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
overridePendingTransition(0, 0);
@@ -34,6 +35,7 @@
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
+ @SuppressWarnings("deprecation")
@Override
public void finish() {
super.finish();
diff --git a/preference/preference-ktx/build.gradle b/preference/preference-ktx/build.gradle
index ebef733..e6e8c4f 100644
--- a/preference/preference-ktx/build.gradle
+++ b/preference/preference-ktx/build.gradle
@@ -43,7 +43,7 @@
}
androidx {
- name = "Android Preferences KTX"
+ name = "Preferences KTX"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Kotlin extensions for preferences"
diff --git a/preference/preference/build.gradle b/preference/preference/build.gradle
index 366ccdc..0e7815d 100644
--- a/preference/preference/build.gradle
+++ b/preference/preference/build.gradle
@@ -64,7 +64,7 @@
}
androidx {
- name = "AndroidX Preference"
+ name = "Preference"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2015"
description = "AndroidX Preference"
diff --git a/print/print/build.gradle b/print/print/build.gradle
index 3b8be7c..6e9dc83 100644
--- a/print/print/build.gradle
+++ b/print/print/build.gradle
@@ -10,7 +10,7 @@
}
androidx {
- name = "Android Support Library Print"
+ name = "Print"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/privacysandbox/ads/ads-adservices/api/current.txt b/privacysandbox/ads/ads-adservices/api/current.txt
index 30cd307..839dec6 100644
--- a/privacysandbox/ads/ads-adservices/api/current.txt
+++ b/privacysandbox/ads/ads-adservices/api/current.txt
@@ -100,13 +100,20 @@
package androidx.privacysandbox.ads.adservices.common {
public final class AdData {
- ctor public AdData(android.net.Uri renderUri, String metadata);
+ ctor public AdData(optional android.net.Uri renderUri, optional String metadata);
method public String getMetadata();
method public android.net.Uri getRenderUri();
property public final String metadata;
property public final android.net.Uri renderUri;
}
+ @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final class AdData.Builder {
+ ctor public AdData.Builder();
+ method public androidx.privacysandbox.ads.adservices.common.AdData build();
+ method public androidx.privacysandbox.ads.adservices.common.AdData.Builder setMetadata(String metadata);
+ method public androidx.privacysandbox.ads.adservices.common.AdData.Builder setRenderUri(android.net.Uri renderUri);
+ }
+
public final class AdSelectionSignals {
ctor public AdSelectionSignals(String signals);
method public String getSignals();
@@ -185,13 +192,20 @@
}
public final class TrustedBiddingData {
- ctor public TrustedBiddingData(android.net.Uri trustedBiddingUri, java.util.List<java.lang.String> trustedBiddingKeys);
+ ctor public TrustedBiddingData(optional android.net.Uri trustedBiddingUri, optional java.util.List<java.lang.String> trustedBiddingKeys);
method public java.util.List<java.lang.String> getTrustedBiddingKeys();
method public android.net.Uri getTrustedBiddingUri();
property public final java.util.List<java.lang.String> trustedBiddingKeys;
property public final android.net.Uri trustedBiddingUri;
}
+ @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final class TrustedBiddingData.Builder {
+ ctor public TrustedBiddingData.Builder();
+ method public androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData build();
+ method public androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData.Builder setTrustedBiddingKeys(java.util.List<java.lang.String> trustedBiddingKeys);
+ method public androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData.Builder setTrustedBiddingUri(android.net.Uri trustedBiddingUri);
+ }
+
}
package androidx.privacysandbox.ads.adservices.measurement {
diff --git a/privacysandbox/ads/ads-adservices/api/public_plus_experimental_current.txt b/privacysandbox/ads/ads-adservices/api/public_plus_experimental_current.txt
index 30cd307..839dec6 100644
--- a/privacysandbox/ads/ads-adservices/api/public_plus_experimental_current.txt
+++ b/privacysandbox/ads/ads-adservices/api/public_plus_experimental_current.txt
@@ -100,13 +100,20 @@
package androidx.privacysandbox.ads.adservices.common {
public final class AdData {
- ctor public AdData(android.net.Uri renderUri, String metadata);
+ ctor public AdData(optional android.net.Uri renderUri, optional String metadata);
method public String getMetadata();
method public android.net.Uri getRenderUri();
property public final String metadata;
property public final android.net.Uri renderUri;
}
+ @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final class AdData.Builder {
+ ctor public AdData.Builder();
+ method public androidx.privacysandbox.ads.adservices.common.AdData build();
+ method public androidx.privacysandbox.ads.adservices.common.AdData.Builder setMetadata(String metadata);
+ method public androidx.privacysandbox.ads.adservices.common.AdData.Builder setRenderUri(android.net.Uri renderUri);
+ }
+
public final class AdSelectionSignals {
ctor public AdSelectionSignals(String signals);
method public String getSignals();
@@ -185,13 +192,20 @@
}
public final class TrustedBiddingData {
- ctor public TrustedBiddingData(android.net.Uri trustedBiddingUri, java.util.List<java.lang.String> trustedBiddingKeys);
+ ctor public TrustedBiddingData(optional android.net.Uri trustedBiddingUri, optional java.util.List<java.lang.String> trustedBiddingKeys);
method public java.util.List<java.lang.String> getTrustedBiddingKeys();
method public android.net.Uri getTrustedBiddingUri();
property public final java.util.List<java.lang.String> trustedBiddingKeys;
property public final android.net.Uri trustedBiddingUri;
}
+ @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final class TrustedBiddingData.Builder {
+ ctor public TrustedBiddingData.Builder();
+ method public androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData build();
+ method public androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData.Builder setTrustedBiddingKeys(java.util.List<java.lang.String> trustedBiddingKeys);
+ method public androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData.Builder setTrustedBiddingUri(android.net.Uri trustedBiddingUri);
+ }
+
}
package androidx.privacysandbox.ads.adservices.measurement {
diff --git a/privacysandbox/ads/ads-adservices/api/restricted_current.txt b/privacysandbox/ads/ads-adservices/api/restricted_current.txt
index 30cd307..839dec6 100644
--- a/privacysandbox/ads/ads-adservices/api/restricted_current.txt
+++ b/privacysandbox/ads/ads-adservices/api/restricted_current.txt
@@ -100,13 +100,20 @@
package androidx.privacysandbox.ads.adservices.common {
public final class AdData {
- ctor public AdData(android.net.Uri renderUri, String metadata);
+ ctor public AdData(optional android.net.Uri renderUri, optional String metadata);
method public String getMetadata();
method public android.net.Uri getRenderUri();
property public final String metadata;
property public final android.net.Uri renderUri;
}
+ @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final class AdData.Builder {
+ ctor public AdData.Builder();
+ method public androidx.privacysandbox.ads.adservices.common.AdData build();
+ method public androidx.privacysandbox.ads.adservices.common.AdData.Builder setMetadata(String metadata);
+ method public androidx.privacysandbox.ads.adservices.common.AdData.Builder setRenderUri(android.net.Uri renderUri);
+ }
+
public final class AdSelectionSignals {
ctor public AdSelectionSignals(String signals);
method public String getSignals();
@@ -185,13 +192,20 @@
}
public final class TrustedBiddingData {
- ctor public TrustedBiddingData(android.net.Uri trustedBiddingUri, java.util.List<java.lang.String> trustedBiddingKeys);
+ ctor public TrustedBiddingData(optional android.net.Uri trustedBiddingUri, optional java.util.List<java.lang.String> trustedBiddingKeys);
method public java.util.List<java.lang.String> getTrustedBiddingKeys();
method public android.net.Uri getTrustedBiddingUri();
property public final java.util.List<java.lang.String> trustedBiddingKeys;
property public final android.net.Uri trustedBiddingUri;
}
+ @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final class TrustedBiddingData.Builder {
+ ctor public TrustedBiddingData.Builder();
+ method public androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData build();
+ method public androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData.Builder setTrustedBiddingKeys(java.util.List<java.lang.String> trustedBiddingKeys);
+ method public androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData.Builder setTrustedBiddingUri(android.net.Uri trustedBiddingUri);
+ }
+
}
package androidx.privacysandbox.ads.adservices.measurement {
diff --git a/privacysandbox/ads/ads-adservices/build.gradle b/privacysandbox/ads/ads-adservices/build.gradle
index 70c6da1..13284aa 100644
--- a/privacysandbox/ads/ads-adservices/build.gradle
+++ b/privacysandbox/ads/ads-adservices/build.gradle
@@ -50,7 +50,7 @@
}
androidx {
- name = "Androidx library for Privacy Preserving APIs."
+ name = "Privacy Sandbox for Ad Services"
type = LibraryType.PUBLISHED_LIBRARY
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2022"
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdDataTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdDataTest.kt
index 501b15f..c548abb 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdDataTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdDataTest.kt
@@ -17,7 +17,10 @@
package androidx.privacysandbox.ads.adservices.common
import android.net.Uri
+import android.os.Build
+import androidx.annotation.RequiresApi
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth
import org.junit.Test
@@ -25,6 +28,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 33)
class AdDataTest {
private val uri: Uri = Uri.parse("abc.com")
private val metadata = "metadata"
@@ -41,4 +45,14 @@
var adData2 = AdData(Uri.parse("abc.com"), "metadata")
Truth.assertThat(adData1 == adData2).isTrue()
}
+
+ @Test
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ fun testBuilderSetters() {
+ val constructed = AdData(uri, metadata)
+ val builder = AdData.Builder()
+ .setRenderUri(uri)
+ .setMetadata(metadata)
+ Truth.assertThat(builder.build()).isEqualTo(constructed)
+ }
}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingDataTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingDataTest.kt
index 1476dae..d26ffe5 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingDataTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingDataTest.kt
@@ -17,7 +17,10 @@
package androidx.privacysandbox.ads.adservices.customaudience
import android.net.Uri
+import android.os.Build
+import androidx.annotation.RequiresApi
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth
import org.junit.Test
@@ -25,6 +28,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 33)
class TrustedBiddingDataTest {
private val uri = Uri.parse("abc.com")
private val keys = listOf("key1", "key2")
@@ -34,4 +38,14 @@
val trustedBiddingData = TrustedBiddingData(uri, keys)
Truth.assertThat(trustedBiddingData.toString()).isEqualTo(result)
}
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ @Test
+ fun testBuilderSetters() {
+ val constructed = TrustedBiddingData(uri, keys)
+ val builder = TrustedBiddingData.Builder()
+ .setTrustedBiddingUri(uri)
+ .setTrustedBiddingKeys(keys)
+ Truth.assertThat(builder.build()).isEqualTo(constructed)
+ }
}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt
index ec458ba..7685d0b 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt
@@ -17,6 +17,8 @@
package androidx.privacysandbox.ads.adservices.common
import android.net.Uri
+import android.os.Build
+import androidx.annotation.RequiresApi
/**
* Represents data specific to an ad that is necessary for ad selection and rendering.
@@ -24,8 +26,8 @@
* @param metadata buyer ad metadata represented as a JSON string
*/
class AdData public constructor(
- val renderUri: Uri,
- val metadata: String
+ val renderUri: Uri = Uri.EMPTY,
+ val metadata: String = ""
) {
/** Checks whether two [AdData] objects contain the same information. */
@@ -47,4 +49,42 @@
override fun toString(): String {
return "AdData: renderUri=$renderUri, metadata='$metadata'"
}
+
+ /** Builder for [AdData] objects. */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ public class Builder {
+ private var renderUri: Uri = Uri.EMPTY
+ private var metadata: String = ""
+
+ /**
+ * Sets the URI that points to the ad's rendering assets. The URI must use HTTPS.
+ *
+ * @param renderUri a URI pointing to the ad's rendering assets
+ */
+ fun setRenderUri(renderUri: Uri): Builder = apply {
+ this.renderUri = renderUri
+ }
+
+ /**
+ * Sets the buyer ad metadata used during the ad selection process.
+ *
+ * @param metadata The metadata should be a valid JSON object serialized as a string.
+ * Metadata represents ad-specific bidding information that will be used during ad selection
+ * as part of bid generation and used in buyer JavaScript logic, which is executed in an
+ * isolated execution environment.
+ *
+ * If the metadata is not a valid JSON object that can be consumed by the buyer's JS, the
+ * ad will not be eligible for ad selection.
+ */
+ fun setMetadata(metadata: String): Builder = apply {
+ this.metadata = metadata
+ }
+
+ /**
+ * Builds an instance of [AdData]
+ */
+ fun build(): AdData {
+ return AdData(renderUri, metadata)
+ }
+ }
}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingData.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingData.kt
index fef0a18..86b4672 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingData.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingData.kt
@@ -17,6 +17,8 @@
package androidx.privacysandbox.ads.adservices.customaudience
import android.net.Uri
+import android.os.Build
+import androidx.annotation.RequiresApi
/**
* Represents data used during the ad selection process to fetch buyer bidding signals from a
@@ -29,8 +31,8 @@
* bidding signals.
*/
class TrustedBiddingData public constructor(
- val trustedBiddingUri: Uri,
- val trustedBiddingKeys: List<String>
+ val trustedBiddingUri: Uri = Uri.EMPTY,
+ val trustedBiddingKeys: List<String> = emptyList()
) {
/**
* @return `true` if two [TrustedBiddingData] objects contain the same information
@@ -53,4 +55,38 @@
return "TrustedBiddingData: trustedBiddingUri=$trustedBiddingUri " +
"trustedBiddingKeys=$trustedBiddingKeys"
}
+
+ /** Builder for [TrustedBiddingData] objects. */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ public class Builder {
+ private var trustedBiddingUri: Uri = Uri.EMPTY
+ private var trustedBiddingKeys: List<String> = emptyList()
+
+ /**
+ * Sets the trusted Bidding Uri
+ *
+ * @param trustedBiddingUri the URI pointing to the trusted key-value server holding bidding
+ * signals. The URI must use HTTPS.
+ */
+ fun setTrustedBiddingUri(trustedBiddingUri: Uri): Builder = apply {
+ this.trustedBiddingUri = trustedBiddingUri
+ }
+
+ /**
+ * Sets the trusted Bidding keys.
+ *
+ * @param trustedBiddingKeys list of keys to query the trusted key-value server with.
+ * This list is permitted to be empty.
+ */
+ fun setTrustedBiddingKeys(trustedBiddingKeys: List<String>): Builder = apply {
+ this.trustedBiddingKeys = trustedBiddingKeys
+ }
+
+ /**
+ * Builds and instance of [TrustedBiddingData]
+ */
+ fun build(): TrustedBiddingData {
+ return TrustedBiddingData(trustedBiddingUri, trustedBiddingKeys)
+ }
+ }
}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/api/api_lint.ignore b/privacysandbox/sdkruntime/sdkruntime-client/api/api_lint.ignore
index 725751e..b3df653 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/api/api_lint.ignore
+++ b/privacysandbox/sdkruntime/sdkruntime-client/api/api_lint.ignore
@@ -1,4 +1,8 @@
// Baseline format: 1.0
+ForbiddenSuperClass: androidx.privacysandbox.sdkruntime.client.activity.SdkActivity:
+ SdkActivity should not extend `Activity`. Activity subclasses are impossible to compose. Expose a composable API instead.
+
+
RegistrationName: androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat#addSdkSandboxProcessDeathCallback(java.util.concurrent.Executor, androidx.privacysandbox.sdkruntime.client.SdkSandboxProcessDeathCallbackCompat):
Callback methods should be named register/unregister; was addSdkSandboxProcessDeathCallback
RegistrationName: androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat#removeSdkSandboxProcessDeathCallback(androidx.privacysandbox.sdkruntime.client.SdkSandboxProcessDeathCallbackCompat):
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/api/current.txt b/privacysandbox/sdkruntime/sdkruntime-client/api/current.txt
index 8d97c00..8f49993 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/api/current.txt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/api/current.txt
@@ -7,6 +7,7 @@
method public java.util.List<androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat> getSandboxedSdks();
method @kotlin.jvm.Throws(exceptionClasses=LoadSdkCompatException::class) public suspend Object? loadSdk(String sdkName, android.os.Bundle params, kotlin.coroutines.Continuation<? super androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat>) throws androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException;
method public void removeSdkSandboxProcessDeathCallback(androidx.privacysandbox.sdkruntime.client.SdkSandboxProcessDeathCallbackCompat callback);
+ method public void startSdkSandboxActivity(android.app.Activity fromActivity, android.os.IBinder sdkActivityToken);
method public void unloadSdk(String sdkName);
field public static final androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat.Companion Companion;
}
@@ -21,3 +22,11 @@
}
+package androidx.privacysandbox.sdkruntime.client.activity {
+
+ public final class SdkActivity extends androidx.activity.ComponentActivity implements androidx.lifecycle.LifecycleOwner {
+ ctor public SdkActivity();
+ }
+
+}
+
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/api/public_plus_experimental_current.txt b/privacysandbox/sdkruntime/sdkruntime-client/api/public_plus_experimental_current.txt
index 8d97c00..8f49993 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/api/public_plus_experimental_current.txt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/api/public_plus_experimental_current.txt
@@ -7,6 +7,7 @@
method public java.util.List<androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat> getSandboxedSdks();
method @kotlin.jvm.Throws(exceptionClasses=LoadSdkCompatException::class) public suspend Object? loadSdk(String sdkName, android.os.Bundle params, kotlin.coroutines.Continuation<? super androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat>) throws androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException;
method public void removeSdkSandboxProcessDeathCallback(androidx.privacysandbox.sdkruntime.client.SdkSandboxProcessDeathCallbackCompat callback);
+ method public void startSdkSandboxActivity(android.app.Activity fromActivity, android.os.IBinder sdkActivityToken);
method public void unloadSdk(String sdkName);
field public static final androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat.Companion Companion;
}
@@ -21,3 +22,11 @@
}
+package androidx.privacysandbox.sdkruntime.client.activity {
+
+ public final class SdkActivity extends androidx.activity.ComponentActivity implements androidx.lifecycle.LifecycleOwner {
+ ctor public SdkActivity();
+ }
+
+}
+
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/api/restricted_current.txt b/privacysandbox/sdkruntime/sdkruntime-client/api/restricted_current.txt
index 8d97c00..906ad91 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/api/restricted_current.txt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/api/restricted_current.txt
@@ -7,6 +7,7 @@
method public java.util.List<androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat> getSandboxedSdks();
method @kotlin.jvm.Throws(exceptionClasses=LoadSdkCompatException::class) public suspend Object? loadSdk(String sdkName, android.os.Bundle params, kotlin.coroutines.Continuation<? super androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat>) throws androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException;
method public void removeSdkSandboxProcessDeathCallback(androidx.privacysandbox.sdkruntime.client.SdkSandboxProcessDeathCallbackCompat callback);
+ method public void startSdkSandboxActivity(android.app.Activity fromActivity, android.os.IBinder sdkActivityToken);
method public void unloadSdk(String sdkName);
field public static final androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat.Companion Companion;
}
@@ -21,3 +22,11 @@
}
+package androidx.privacysandbox.sdkruntime.client.activity {
+
+ public final class SdkActivity extends androidx.activity.ComponentActivity {
+ ctor public SdkActivity();
+ }
+
+}
+
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
index a0f618d..486fe73 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
+++ b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
@@ -25,11 +25,12 @@
dependencies {
api(libs.kotlinStdlib)
api(libs.kotlinCoroutinesCore)
- implementation("androidx.core:core-ktx:1.8.0")
+ implementation("androidx.core:core-ktx:1.12.0-alpha03")
api project(path: ':privacysandbox:sdkruntime:sdkruntime-core')
- implementation("androidx.core:core:1.8.0")
+ implementation("androidx.core:core:1.12.0-alpha03")
+ implementation(projectOrArtifact(":activity:activity"))
testImplementation(libs.junit)
testImplementation(libs.truth)
@@ -42,6 +43,7 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.junit)
+ androidTestImplementation(project(':internal-testutils-runtime'))
androidTestImplementation(project(":internal-testutils-truth")) // for assertThrows
androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/lint-baseline.xml b/privacysandbox/sdkruntime/sdkruntime-client/lint-baseline.xml
index 7b70e67..59ae122 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/lint-baseline.xml
+++ b/privacysandbox/sdkruntime/sdkruntime-client/lint-baseline.xml
@@ -1,23 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-beta03" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-beta03)" variant="all" version="8.0.0-beta03">
-
- <issue
- id="NewApi"
- message="Call requires API level 33 (current min is 14): `android.app.sdksandbox.SdkSandboxManager#getSandboxedSdks`"
- errorLine1=" `when`(sdkSandboxManager.sandboxedSdks)"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatSandboxedTest.kt"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 33 (current min is 14): `android.app.sdksandbox.SdkSandboxManager#getSandboxedSdks`"
- errorLine1=" `when`(sdkSandboxManager.sandboxedSdks)"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatSandboxedTest.kt"/>
- </issue>
+<issues format="6" by="lint 8.1.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta01)" variant="all" version="8.1.0-beta01">
<issue
id="BanThreadSleep"
@@ -46,4 +28,13 @@
file="src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SandboxControllerInjector.kt"/>
</issue>
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" return if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompat.kt"/>
+ </issue>
+
</issues>
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/AndroidManifest.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/AndroidManifest.xml
index 3e3ea90..98769ac 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/AndroidManifest.xml
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/AndroidManifest.xml
@@ -14,4 +14,14 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <application>
+ <activity
+ android:name="androidx.privacysandbox.sdkruntime.client.EmptyActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
</manifest>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdkTable.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdkTable.xml
index c72c1a2..3ac54e2 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdkTable.xml
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdkTable.xml
@@ -24,6 +24,10 @@
<package-name>androidx.privacysandbox.sdkruntime.test.v2</package-name>
</runtime-enabled-sdk>
<runtime-enabled-sdk>
+ <compat-config-path>RuntimeEnabledSdks/V3/CompatSdkConfig.xml</compat-config-path>
+ <package-name>androidx.privacysandbox.sdkruntime.test.v3</package-name>
+ </runtime-enabled-sdk>
+ <runtime-enabled-sdk>
<compat-config-path>RuntimeEnabledSdks/InvalidEntryPointSdkConfig.xml</compat-config-path>
<package-name>androidx.privacysandbox.sdkruntime.test.invalidEntryPoint</package-name>
</runtime-enabled-sdk>
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V3/CompatSdkCode.md b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V3/CompatSdkCode.md
new file mode 100644
index 0000000..750e2a3
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V3/CompatSdkCode.md
@@ -0,0 +1,263 @@
+Test sdk that was built with V3 library.
+
+DO NOT RECOMPILE WITH ANY CHANGES TO LIBRARY CLASSES.
+Main purpose of that provider is to test that old core versions could be loaded by new client.
+
+classes.dex built from:
+
+1) androidx.privacysandbox.sdkruntime.core.Versions
+@Keep
+object Versions {
+
+ const val API_VERSION = 3
+
+ @JvmField
+ var CLIENT_VERSION: Int? = null
+
+ @JvmStatic
+ fun handShake(clientVersion: Int): Int {
+ CLIENT_VERSION = clientVersion
+ return API_VERSION
+ }
+}
+
+2) androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
+abstract class SandboxedSdkProviderCompat {
+ var context: Context? = null
+ private set
+
+ fun attachContext(context: Context) {
+ check(this.context == null) { "Context already set" }
+ this.context = context
+ }
+
+ @Throws(LoadSdkCompatException::class)
+ abstract fun onLoadSdk(params: Bundle): SandboxedSdkCompat
+
+ open fun beforeUnloadSdk() {}
+
+ abstract fun getView(
+ windowContext: Context,
+ params: Bundle,
+ width: Int,
+ height: Int
+ ): View
+}
+
+3) androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+class SandboxedSdkCompat private constructor(
+ private val sdkImpl: SandboxedSdkImpl
+) {
+
+ constructor(sdkInterface: IBinder) : this(sdkInterface, sdkInfo = null)
+
+ @Keep
+ constructor(
+ sdkInterface: IBinder,
+ sdkInfo: SandboxedSdkInfo?
+ ) : this(CompatImpl(sdkInterface, sdkInfo))
+
+ fun getInterface() = sdkImpl.getInterface()
+
+ fun getSdkInfo(): SandboxedSdkInfo? = sdkImpl.getSdkInfo()
+
+ internal interface SandboxedSdkImpl {
+ fun getInterface(): IBinder?
+ fun getSdkInfo(): SandboxedSdkInfo?
+ }
+
+ private class CompatImpl(
+ private val sdkInterface: IBinder,
+ private val sdkInfo: SandboxedSdkInfo?
+ ) : SandboxedSdkImpl {
+
+ override fun getInterface(): IBinder {
+ return sdkInterface
+ }
+
+ override fun getSdkInfo(): SandboxedSdkInfo? = sdkInfo
+ }
+}
+
+4) androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
+class LoadSdkCompatException : Exception {
+
+ val loadSdkErrorCode: Int
+
+ val extraInformation: Bundle
+
+ @JvmOverloads
+ constructor(
+ loadSdkErrorCode: Int,
+ message: String?,
+ cause: Throwable?,
+ extraInformation: Bundle = Bundle()
+ ) : super(message, cause) {
+ this.loadSdkErrorCode = loadSdkErrorCode
+ this.extraInformation = extraInformation
+ }
+
+ constructor(
+ cause: Throwable,
+ extraInfo: Bundle
+ ) : this(LOAD_SDK_SDK_DEFINED_ERROR, "", cause, extraInfo)
+
+ companion object {
+ const val LOAD_SDK_SDK_DEFINED_ERROR = 102
+ }
+}
+
+5) androidx.privacysandbox.sdkruntime.core.SandboxedSdkInfo
+class SandboxedSdkInfo(
+ val name: String,
+ val version: Long
+) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+ other as SandboxedSdkInfo
+ if (name != other.name) return false
+ if (version != other.version) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = name.hashCode()
+ result = 31 * result + version.hashCode()
+ return result
+ }
+}
+
+6) androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
+interface ActivityHolder : LifecycleOwner {
+ fun getActivity(): Activity
+ fun getOnBackPressedDispatcher(): OnBackPressedDispatcher
+}
+
+7) androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
+interface SdkSandboxActivityHandlerCompat {
+ fun onActivityCreated(activityHolder: ActivityHolder)
+}
+
+8) androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
+class SdkSandboxControllerCompat internal constructor(
+ private val controllerImpl: SandboxControllerImpl
+) {
+ fun getSandboxedSdks(): List<SandboxedSdkCompat> =
+ controllerImpl.getSandboxedSdks()
+
+ fun registerSdkSandboxActivityHandler(handlerCompat: SdkSandboxActivityHandlerCompat):
+ IBinder = controllerImpl.registerSdkSandboxActivityHandler(handlerCompat)
+
+ fun unregisterSdkSandboxActivityHandler(handlerCompat: SdkSandboxActivityHandlerCompat) =
+ controllerImpl.unregisterSdkSandboxActivityHandler(handlerCompat)
+
+ interface SandboxControllerImpl {
+ fun getSandboxedSdks(): List<SandboxedSdkCompat>
+ fun registerSdkSandboxActivityHandler(handlerCompat: SdkSandboxActivityHandlerCompat):
+ IBinder
+ fun unregisterSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ )
+ }
+
+ companion object {
+ private var localImpl: SandboxControllerImpl? = null
+ @JvmStatic
+ fun from(context: Context): SdkSandboxControllerCompat {
+ val clientVersion = Versions.CLIENT_VERSION
+ if (clientVersion != null) {
+ val implFromClient = localImpl
+ if (implFromClient != null) {
+ return SdkSandboxControllerCompat(LocalImpl(implFromClient, clientVersion))
+ }
+ }
+ throw IllegalStateException("Should be loaded locally")
+ }
+ @JvmStatic
+ @Keep
+ fun injectLocalImpl(impl: SandboxControllerImpl) {
+ check(localImpl == null) { "Local implementation already injected" }
+ localImpl = impl
+ }
+ }
+}
+
+9) androidx.privacysandbox.sdkruntime.core.controller.impl.LocalImpl
+internal class LocalImpl(
+ private val implFromClient: SdkSandboxControllerCompat.SandboxControllerImpl,
+ private val clientVersion: Int
+) : SdkSandboxControllerCompat.SandboxControllerImpl {
+ override fun getSandboxedSdks(): List<SandboxedSdkCompat> {
+ return implFromClient.getSandboxedSdks()
+ }
+
+ override fun registerSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ): IBinder {
+ if (clientVersion < 3) {
+ throw UnsupportedOperationException(
+ "Client library version doesn't support SdkActivities"
+ )
+ }
+ return implFromClient.registerSdkSandboxActivityHandler(handlerCompat)
+ }
+
+ override fun unregisterSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ) {
+ if (clientVersion < 3) {
+ throw UnsupportedOperationException(
+ "Client library version doesn't support SdkActivities"
+ )
+ }
+ implFromClient.unregisterSdkSandboxActivityHandler(handlerCompat)
+ }
+}
+
+10) androidx.privacysandbox.sdkruntime.test.v3.CompatProvider
+class CompatProvider : SandboxedSdkProviderCompat() {
+
+ @JvmField
+ var onLoadSdkBinder: Binder? = null
+
+ @JvmField
+ var lastOnLoadSdkParams: Bundle? = null
+
+ @JvmField
+ var isBeforeUnloadSdkCalled = false
+
+ @Throws(LoadSdkCompatException::class)
+ override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
+ val result = SdkImpl(context!!)
+ onLoadSdkBinder = result
+ if (params.getBoolean("needFail", false)) {
+ throw LoadSdkCompatException(RuntimeException(), params)
+ }
+ return SandboxedSdkCompat(result)
+ }
+
+ override fun beforeUnloadSdk() {
+ isBeforeUnloadSdkCalled = true
+ }
+
+ override fun getView(
+ windowContext: Context, params: Bundle, width: Int,
+ height: Int
+ ): View {
+ return View(windowContext)
+ }
+
+ class SdkImpl(
+ private val context: Context
+ ) : Binder() {
+ fun getSandboxedSdks(): List<SandboxedSdkCompat> =
+ SdkSandboxControllerCompat.from(context).getSandboxedSdks()
+ fun registerSdkSandboxActivityHandler(handler: SdkSandboxActivityHandlerCompat): IBinder =
+ SdkSandboxControllerCompat.from(context).registerSdkSandboxActivityHandler(handler)
+ fun unregisterSdkSandboxActivityHandler(handler: SdkSandboxActivityHandlerCompat) {
+ SdkSandboxControllerCompat.from(context).unregisterSdkSandboxActivityHandler(handler)
+ }
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V3/CompatSdkConfig.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V3/CompatSdkConfig.xml
new file mode 100644
index 0000000..5c23d99
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V3/CompatSdkConfig.xml
@@ -0,0 +1,19 @@
+<!--
+ 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.
+ -->
+<compat-config>
+ <compat-entrypoint>androidx.privacysandbox.sdkruntime.test.v3.CompatProvider</compat-entrypoint>
+ <dex-path>RuntimeEnabledSdks/V3/classes.dex</dex-path>
+</compat-config>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V3/classes.dex b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V3/classes.dex
new file mode 100644
index 0000000..9740bdf
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V3/classes.dex
Binary files differ
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/EmptyActivity.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/EmptyActivity.kt
new file mode 100644
index 0000000..f7d19b4
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/EmptyActivity.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.privacysandbox.sdkruntime.client
+
+import androidx.activity.ComponentActivity
+
+/**
+ * Activity for components tests.
+ * [androidx.privacysandbox.sdkruntime.client.activity.SdkActivity] can't be used for most tests as
+ * it will be moved to finished state during creation when no valid token provided.
+ */
+class EmptyActivity : ComponentActivity()
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatSandboxedTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatSandboxedTest.kt
index ab6b951..a56a801 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatSandboxedTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatSandboxedTest.kt
@@ -16,6 +16,7 @@
package androidx.privacysandbox.sdkruntime.client
+import android.app.Activity
import android.app.sdksandbox.LoadSdkException
import android.app.sdksandbox.SandboxedSdk
import android.app.sdksandbox.SdkSandboxManager
@@ -23,6 +24,7 @@
import android.os.Binder
import android.os.Build
import android.os.Bundle
+import android.os.IBinder
import android.os.OutcomeReceiver
import android.os.ext.SdkExtensions.AD_SERVICES
import androidx.annotation.RequiresExtension
@@ -169,6 +171,19 @@
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+ fun startSdkSandboxActivity_whenSandboxAvailable_delegateToPlatform() {
+ val sdkSandboxManager = mockSandboxManager(mContext)
+ val managerCompat = SdkSandboxManagerCompat.from(mContext)
+
+ val fromActivityMock = mock(Activity::class.java)
+ val tokenMock = mock(IBinder::class.java)
+ managerCompat.startSdkSandboxActivity(fromActivityMock, tokenMock)
+
+ verify(sdkSandboxManager).startSdkSandboxActivity(fromActivityMock, tokenMock)
+ }
+
+ @Test
fun removeSdkSandboxProcessDeathCallback_whenSandboxAvailable_removeAddedCallback() {
val sdkSandboxManager = mockSandboxManager(mContext)
val managerCompat = SdkSandboxManagerCompat.from(mContext)
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatTest.kt
index 3a49aa7..9166446 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatTest.kt
@@ -15,10 +15,14 @@
*/
package androidx.privacysandbox.sdkruntime.client
+import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
+import android.os.Binder
import android.os.Build
import android.os.Bundle
+import androidx.privacysandbox.sdkruntime.client.activity.SdkActivity
+import androidx.privacysandbox.sdkruntime.client.loader.CatchingSdkActivityHandler
import androidx.privacysandbox.sdkruntime.client.loader.asTestSdk
import androidx.privacysandbox.sdkruntime.client.loader.extractSdkProviderFieldValue
import androidx.privacysandbox.sdkruntime.core.AdServicesInfo
@@ -26,10 +30,12 @@
import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException.Companion.LOAD_SDK_INTERNAL_ERROR
import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException.Companion.LOAD_SDK_SDK_DEFINED_ERROR
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkInfo
+import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
+import androidx.testutils.withActivity
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.After
@@ -285,6 +291,44 @@
@Ignore("b/277764220")
@Test
+ fun startSdkSandboxActivity_whenSandboxNotAvailable_dontDelegateToSandbox() {
+ // TODO(b/262577044) Replace with @SdkSuppress after supporting maxExtensionVersion
+ assumeTrue("Requires Sandbox API not available", isSandboxApiNotAvailable())
+
+ val context = spy(ApplicationProvider.getApplicationContext<Context>())
+ val managerCompat = SdkSandboxManagerCompat.from(context)
+
+ val fromActivitySpy = Mockito.mock(Activity::class.java)
+ managerCompat.startSdkSandboxActivity(fromActivitySpy, Binder())
+
+ verify(context, Mockito.never()).getSystemService(any())
+ }
+
+ @Test
+ fun startSdkSandboxActivity_startLocalSdkActivity() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val managerCompat = SdkSandboxManagerCompat.from(context)
+
+ val localSdk = runBlocking {
+ managerCompat.loadSdk("androidx.privacysandbox.sdkruntime.test.v3", Bundle())
+ }
+
+ val handler = CatchingSdkActivityHandler()
+
+ val testSdk = localSdk.asTestSdk()
+ val token = testSdk.registerSdkSandboxActivityHandler(handler)
+
+ with(ActivityScenario.launch(EmptyActivity::class.java)) {
+ withActivity {
+ managerCompat.startSdkSandboxActivity(this, token)
+ }
+ }
+
+ val activityHolder = handler.waitForActivity()
+ assertThat(activityHolder.getActivity()).isInstanceOf(SdkActivity::class.java)
+ }
+
+ @Test
fun sdkController_getSandboxedSdks_returnsLocallyLoadedSdks() {
val context = ApplicationProvider.getApplicationContext<Context>()
val managerCompat = SdkSandboxManagerCompat.from(context)
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/activity/LocalSdkActivityStarterTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/activity/LocalSdkActivityStarterTest.kt
new file mode 100644
index 0000000..7262c83
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/activity/LocalSdkActivityStarterTest.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.activity
+
+import android.os.Binder
+import androidx.privacysandbox.sdkruntime.client.EmptyActivity
+import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
+import androidx.test.core.app.ActivityScenario
+import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import org.junit.Test
+
+class LocalSdkActivityStarterTest {
+
+ @Test
+ fun tryStart_whenHandlerRegistered_startSdkActivityAndReturnTrue() {
+ val handler = TestHandler()
+ val registeredToken = LocalSdkActivityHandlerRegistry.register(handler)
+
+ val startResult = with(ActivityScenario.launch(EmptyActivity::class.java)) {
+ withActivity {
+ LocalSdkActivityStarter.tryStart(this, registeredToken)
+ }
+ }
+
+ assertThat(startResult).isTrue()
+
+ val activityHolder = handler.waitForActivity()
+ assertThat(activityHolder.getActivity()).isInstanceOf(SdkActivity::class.java)
+ }
+
+ @Test
+ fun tryStart_whenHandlerNotRegistered_ReturnFalse() {
+ val unregisteredToken = Binder()
+
+ val startResult = with(ActivityScenario.launch(EmptyActivity::class.java)) {
+ withActivity {
+ LocalSdkActivityStarter.tryStart(this, unregisteredToken)
+ }
+ }
+
+ assertThat(startResult).isFalse()
+ }
+
+ private class TestHandler : SdkSandboxActivityHandlerCompat {
+ var result: ActivityHolder? = null
+ var async = CountDownLatch(1)
+
+ override fun onActivityCreated(activityHolder: ActivityHolder) {
+ result = activityHolder
+ async.countDown()
+ }
+
+ fun waitForActivity(): ActivityHolder {
+ async.await()
+ return result!!
+ }
+ }
+}
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/controller/LocalControllerTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/controller/LocalControllerTest.kt
index b44ba98..cf99675 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/controller/LocalControllerTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/controller/LocalControllerTest.kt
@@ -18,8 +18,11 @@
import android.os.Binder
import android.os.Bundle
+import androidx.privacysandbox.sdkruntime.client.activity.LocalSdkActivityHandlerRegistry
import androidx.privacysandbox.sdkruntime.client.loader.LocalSdkProvider
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
@@ -54,6 +57,35 @@
assertThat(result).containsExactly(sandboxedSdk)
}
+ @Test
+ fun registerSdkSandboxActivityHandler_delegateToLocalSdkActivityHandlerRegistry() {
+ val handler = object : SdkSandboxActivityHandlerCompat {
+ override fun onActivityCreated(activityHolder: ActivityHolder) {
+ // do nothing
+ }
+ }
+
+ val token = controller.registerSdkSandboxActivityHandler(handler)
+
+ val registeredHandler = LocalSdkActivityHandlerRegistry.getHandlerByToken(token)
+ assertThat(registeredHandler).isSameInstanceAs(handler)
+ }
+
+ @Test
+ fun unregisterSdkSandboxActivityHandler_delegateToLocalSdkActivityHandlerRegistry() {
+ val handler = object : SdkSandboxActivityHandlerCompat {
+ override fun onActivityCreated(activityHolder: ActivityHolder) {
+ // do nothing
+ }
+ }
+
+ val token = controller.registerSdkSandboxActivityHandler(handler)
+ controller.unregisterSdkSandboxActivityHandler(handler)
+
+ val registeredHandler = LocalSdkActivityHandlerRegistry.getHandlerByToken(token)
+ assertThat(registeredHandler).isNull()
+ }
+
private class NoOpSdkProvider : LocalSdkProvider(Any()) {
override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
throw IllegalStateException("Unexpected call")
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkProviderTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkProviderTest.kt
index f34de7b1..c7910dc 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkProviderTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkProviderTest.kt
@@ -18,7 +18,10 @@
import android.content.Context
import android.os.Binder
import android.os.Bundle
+import android.os.IBinder
import android.view.View
+import androidx.privacysandbox.sdkruntime.client.EmptyActivity
+import androidx.privacysandbox.sdkruntime.client.activity.ComponentActivityHolder
import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
import androidx.privacysandbox.sdkruntime.client.loader.impl.SandboxedSdkContextCompat
import androidx.privacysandbox.sdkruntime.client.loader.storage.TestLocalSdkStorage
@@ -28,9 +31,12 @@
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkInfo
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
import androidx.privacysandbox.sdkruntime.core.Versions
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
+import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SmallTest
+import androidx.testutils.withActivity
import com.google.common.truth.Truth.assertThat
import dalvik.system.BaseDexClassLoader
import java.io.File
@@ -124,6 +130,47 @@
assertThat(result.getSdkVersion()).isEqualTo(expectedResult.getSdkInfo()!!.version)
}
+ @Test
+ fun registerSdkSandboxActivityHandler_delegateToSdkController() {
+ assumeTrue(
+ "Requires Versions.API_VERSION >= 3",
+ sdkVersion >= 3
+ )
+
+ val catchingHandler = CatchingSdkActivityHandler()
+
+ val testSdk = loadedSdk.loadTestSdk()
+ val token = testSdk.registerSdkSandboxActivityHandler(catchingHandler)
+ val localHandler = controller.sdkActivityHandlers[token]!!
+
+ with(ActivityScenario.launch(EmptyActivity::class.java)) {
+ withActivity {
+ val activityHolder = ComponentActivityHolder(this)
+ localHandler.onActivityCreated(activityHolder)
+
+ val receivedActivityHolder = catchingHandler.result!!
+ val receivedActivity = receivedActivityHolder.getActivity()
+ assertThat(receivedActivity).isSameInstanceAs(activityHolder.getActivity())
+ }
+ }
+ }
+
+ @Test
+ fun unregisterSdkSandboxActivityHandler_delegateToSdkController() {
+ assumeTrue(
+ "Requires Versions.API_VERSION >= 3",
+ sdkVersion >= 3
+ )
+
+ val handler = CatchingSdkActivityHandler()
+
+ val testSdk = loadedSdk.loadTestSdk()
+ val token = testSdk.registerSdkSandboxActivityHandler(handler)
+ testSdk.unregisterSdkSandboxActivityHandler(handler)
+
+ assertThat(controller.sdkActivityHandlers[token]).isNull()
+ }
+
class CurrentVersionProviderLoadTest : SandboxedSdkProviderCompat() {
@JvmField
var onLoadSdkBinder: Binder? = null
@@ -166,6 +213,13 @@
) : Binder() {
fun getSandboxedSdks(): List<SandboxedSdkCompat> =
SdkSandboxControllerCompat.from(context).getSandboxedSdks()
+
+ fun registerSdkSandboxActivityHandler(handler: SdkSandboxActivityHandlerCompat): IBinder =
+ SdkSandboxControllerCompat.from(context).registerSdkSandboxActivityHandler(handler)
+
+ fun unregisterSdkSandboxActivityHandler(handler: SdkSandboxActivityHandlerCompat) {
+ SdkSandboxControllerCompat.from(context).unregisterSdkSandboxActivityHandler(handler)
+ }
}
internal class TestClassLoaderFactory(
@@ -214,6 +268,11 @@
2,
"RuntimeEnabledSdks/V2/classes.dex",
"androidx.privacysandbox.sdkruntime.test.v2.CompatProvider"
+ ),
+ TestSdkInfo(
+ 3,
+ "RuntimeEnabledSdks/V3/classes.dex",
+ "androidx.privacysandbox.sdkruntime.test.v3.CompatProvider"
)
)
@@ -222,14 +281,12 @@
fun params(): List<Array<Any>> = buildList {
assertThat(SDKS.size).isEqualTo(Versions.API_VERSION)
- val controller = TestStubController()
-
- val assetsSdkLoader = createAssetsSdkLoader(controller)
for (i in SDKS.indices) {
val sdk = SDKS[i]
assertThat(sdk.apiVersion).isEqualTo(i + 1)
- val loadedSdk = assetsSdkLoader.loadSdk(sdk.localSdkConfig)
+ val controller = TestStubController()
+ val loadedSdk = loadTestSdkFromAssets(sdk.localSdkConfig, controller)
assertThat(loadedSdk.extractApiVersion())
.isEqualTo(sdk.apiVersion)
@@ -244,6 +301,7 @@
}
// add SDK loaded from test sources
+ val controller = TestStubController()
add(
arrayOf(
"BuiltFromSource",
@@ -275,26 +333,46 @@
)
}
- private fun createAssetsSdkLoader(controller: TestStubController): SdkLoader {
+ private fun loadTestSdkFromAssets(
+ sdkConfig: LocalSdkConfig,
+ controller: TestStubController
+ ): LocalSdkProvider {
val context = ApplicationProvider.getApplicationContext<Context>()
val testStorage = TestLocalSdkStorage(
context,
rootFolder = File(context.cacheDir, "LocalSdkTest")
)
- return SdkLoader(
+ val sdkLoader = SdkLoader(
TestClassLoaderFactory(testStorage),
context,
controller
)
+ return sdkLoader.loadSdk(sdkConfig)
}
}
internal class TestStubController : SdkSandboxControllerCompat.SandboxControllerImpl {
var sandboxedSdksResult: List<SandboxedSdkCompat> = emptyList()
+ var sdkActivityHandlers: MutableMap<IBinder, SdkSandboxActivityHandlerCompat> =
+ mutableMapOf()
override fun getSandboxedSdks(): List<SandboxedSdkCompat> {
return sandboxedSdksResult
}
+
+ override fun registerSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ): IBinder {
+ val token = Binder()
+ sdkActivityHandlers[token] = handlerCompat
+ return token
+ }
+
+ override fun unregisterSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ) {
+ sdkActivityHandlers.values.remove(handlerCompat)
+ }
}
}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkTestUtils.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkTestUtils.kt
index def9887..a23c0ad 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkTestUtils.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkTestUtils.kt
@@ -16,12 +16,16 @@
package androidx.privacysandbox.sdkruntime.client.loader
+import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.os.IBinder
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
import androidx.privacysandbox.sdkruntime.core.Versions
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
+import java.lang.reflect.Proxy
+import java.util.concurrent.CountDownLatch
import kotlin.reflect.cast
/**
@@ -72,6 +76,8 @@
* Underlying TestSDK should implement and delegate to
* [androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat]:
* 1) getSandboxedSdks() : List<SandboxedSdkCompat>
+ * 2) registerSdkSandboxActivityHandler(SdkSandboxActivityHandlerCompat) : IBinder
+ * 3) unregisterSdkSandboxActivityHandler(SdkSandboxActivityHandlerCompat)
*/
internal class TestSdkWrapper(
private val sdk: Any
@@ -82,6 +88,54 @@
) as List<*>
return sdks.map { SandboxedSdkWrapper(it!!) }
}
+
+ fun registerSdkSandboxActivityHandler(handler: CatchingSdkActivityHandler): IBinder {
+ val classLoader = sdk.javaClass.classLoader!!
+ val activityHandlerClass = Class.forName(
+ SdkSandboxActivityHandlerCompat::class.java.name,
+ false,
+ classLoader
+ )
+
+ val proxy = Proxy.newProxyInstance(
+ classLoader,
+ arrayOf(activityHandlerClass)
+ ) { proxy, method, args ->
+ when (method.name) {
+ "hashCode" -> hashCode()
+ "equals" -> proxy === args[0]
+ "onActivityCreated" -> handler.setResult(args[0])
+ else -> {
+ throw UnsupportedOperationException(
+ "Unexpected method call object:$proxy, method: $method, args: $args"
+ )
+ }
+ }
+ }
+
+ val registerMethod = sdk.javaClass
+ .getMethod("registerSdkSandboxActivityHandler", activityHandlerClass)
+
+ val token = registerMethod.invoke(sdk, proxy) as IBinder
+ handler.proxy = proxy
+
+ return token
+ }
+
+ fun unregisterSdkSandboxActivityHandler(handler: CatchingSdkActivityHandler) {
+ val classLoader = sdk.javaClass.classLoader!!
+ val activityHandlerClass = Class.forName(
+ SdkSandboxActivityHandlerCompat::class.java.name,
+ false,
+ classLoader
+ )
+
+ val unregisterMethod = sdk.javaClass
+ .getMethod("unregisterSdkSandboxActivityHandler", activityHandlerClass)
+
+ unregisterMethod.invoke(sdk, handler.proxy)
+ handler.proxy = null
+ }
}
/**
@@ -124,6 +178,39 @@
}
/**
+ * ActivityHandler to use with [TestSdkWrapper.registerSdkSandboxActivityHandler].
+ * Store received ActivityHolder.
+ */
+internal class CatchingSdkActivityHandler {
+ var proxy: Any? = null
+ var result: ActivityHolderWrapper? = null
+ val async = CountDownLatch(1)
+
+ fun waitForActivity(): ActivityHolderWrapper {
+ async.await()
+ return result!!
+ }
+}
+
+private fun CatchingSdkActivityHandler.setResult(activityHolder: Any) {
+ result = ActivityHolderWrapper(activityHolder)
+ async.countDown()
+}
+
+/**
+ * Reflection wrapper for [androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder]
+ */
+internal class ActivityHolderWrapper(
+ private val activityHolder: Any
+) {
+ fun getActivity(): Activity {
+ return activityHolder.callMethod(
+ methodName = "getActivity"
+ ) as Activity
+ }
+}
+
+/**
* Load SDK and wrap it as TestSDK.
* @see [TestSdkWrapper]
*/
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
index 341d08f..566fca8 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
@@ -17,10 +17,12 @@
import android.content.Context
import android.os.Build
+import android.os.IBinder
import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
import androidx.privacysandbox.sdkruntime.client.config.ResourceRemappingConfig
import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
import androidx.privacysandbox.sdkruntime.core.Versions
import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
import androidx.test.core.app.ApplicationProvider
@@ -149,5 +151,17 @@
override fun getSandboxedSdks(): List<SandboxedSdkCompat> {
throw UnsupportedOperationException("NoOp")
}
+
+ override fun registerSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ): IBinder {
+ throw UnsupportedOperationException("NoOp")
+ }
+
+ override fun unregisterSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ) {
+ throw UnsupportedOperationException("NoOp")
+ }
}
}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/ActivityHolderProxyFactoryTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/ActivityHolderProxyFactoryTest.kt
new file mode 100644
index 0000000..d198c00
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/ActivityHolderProxyFactoryTest.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.privacysandbox.sdkruntime.client.loader.impl.injector
+
+import androidx.privacysandbox.sdkruntime.client.EmptyActivity
+import androidx.privacysandbox.sdkruntime.client.activity.ComponentActivityHolder
+import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
+import androidx.test.core.app.ActivityScenario
+import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+
+class ActivityHolderProxyFactoryTest {
+
+ private lateinit var factory: ActivityHolderProxyFactory
+
+ @Before
+ fun setUp() {
+ factory = ActivityHolderProxyFactory.createFor(javaClass.classLoader!!)
+ }
+
+ @Test
+ fun createProxyFor_RetrievesActivityFromOriginalActivityHolder() {
+ with(ActivityScenario.launch(EmptyActivity::class.java)) {
+ withActivity {
+ val activityHolder = ComponentActivityHolder(this)
+ val proxy = factory.createProxyFor(activityHolder) as ActivityHolder
+ assertThat(proxy.getActivity()).isSameInstanceAs(activityHolder.getActivity())
+ }
+ }
+ }
+
+ @Suppress("ReplaceCallWithBinaryOperator") // Explicitly testing equals on proxy
+ @Test
+ fun createProxyFor_CreatesProxyWithValidEqualsAndHashCode() {
+ with(ActivityScenario.launch(EmptyActivity::class.java)) {
+ withActivity {
+ val activityHolder = ComponentActivityHolder(this)
+ val proxy = factory.createProxyFor(activityHolder)
+ assertThat(proxy.equals(proxy)).isTrue()
+ assertThat(proxy.hashCode()).isEqualTo(proxy.hashCode())
+ assertThat(proxy.toString()).isEqualTo(proxy.toString())
+ }
+ }
+ }
+
+ @Test
+ fun getOnBackPressedDispatcher_DoesntThrow() {
+ with(ActivityScenario.launch(EmptyActivity::class.java)) {
+ withActivity {
+ val activityHolder = ComponentActivityHolder(this)
+ val proxy = factory.createProxyFor(activityHolder) as ActivityHolder
+ proxy.getOnBackPressedDispatcher()
+ }
+ }
+ }
+
+ @Test
+ fun getLifecycle_DoesntThrow() {
+ with(ActivityScenario.launch(EmptyActivity::class.java)) {
+ withActivity {
+ val activityHolder = ComponentActivityHolder(this)
+ val proxy = factory.createProxyFor(activityHolder) as ActivityHolder
+ proxy.lifecycle
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/SdkActivityHandlerWrapperTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/SdkActivityHandlerWrapperTest.kt
new file mode 100644
index 0000000..a4622db
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/SdkActivityHandlerWrapperTest.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader.impl.injector
+
+import androidx.privacysandbox.sdkruntime.client.EmptyActivity
+import androidx.privacysandbox.sdkruntime.client.activity.ComponentActivityHolder
+import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SdkActivityHandlerWrapperTest {
+
+ private lateinit var wrapperFactory: SdkActivityHandlerWrapper
+
+ @Before
+ fun setUp() {
+ wrapperFactory = SdkActivityHandlerWrapper.createFor(javaClass.classLoader!!)
+ }
+
+ @Test
+ fun wrapSdkSandboxActivityHandlerCompat_passActivityToOriginalHandler() {
+ val catchingHandler = TestHandler()
+
+ val wrappedHandler = wrapperFactory.wrapSdkSandboxActivityHandlerCompat(catchingHandler)
+
+ with(ActivityScenario.launch(EmptyActivity::class.java)) {
+ withActivity {
+ val activityHolder = ComponentActivityHolder(this)
+
+ wrappedHandler.onActivityCreated(activityHolder)
+ val receivedActivityHolder = catchingHandler.result!!
+
+ assertThat(receivedActivityHolder.getActivity())
+ .isSameInstanceAs(activityHolder.getActivity())
+ }
+ }
+ }
+
+ private class TestHandler : SdkSandboxActivityHandlerCompat {
+ var result: ActivityHolder? = null
+
+ override fun onActivityCreated(activityHolder: ActivityHolder) {
+ result = activityHolder
+ }
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/AndroidManifest.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ff1665b
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <application>
+ <activity
+ android:name="androidx.privacysandbox.sdkruntime.client.activity.SdkActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompat.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompat.kt
index 5104e2a..0735425 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompat.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompat.kt
@@ -16,16 +16,21 @@
package androidx.privacysandbox.sdkruntime.client
import android.annotation.SuppressLint
+import android.app.Activity
import android.app.sdksandbox.LoadSdkException
import android.app.sdksandbox.SandboxedSdk
import android.app.sdksandbox.SdkSandboxManager
import android.content.Context
import android.os.Bundle
+import android.os.IBinder
import android.os.ext.SdkExtensions.AD_SERVICES
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresExtension
+import androidx.annotation.OptIn
+import androidx.core.os.BuildCompat
import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.sdkruntime.client.activity.LocalSdkActivityStarter
import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfigsHolder
import androidx.privacysandbox.sdkruntime.client.controller.LocalController
import androidx.privacysandbox.sdkruntime.client.controller.LocallyLoadedSdks
@@ -204,6 +209,26 @@
return platformResult + localResult
}
+ /**
+ * Starts an [Activity] in the SDK sandbox.
+ *
+ * This function will start a new [Activity] in the same task of the passed `fromActivity` and
+ * pass it to the SDK that shared the passed `sdkActivityToken` that identifies a request from
+ * that SDK to stat this [Activity].
+ *
+ * @param fromActivity the [Activity] will be used to start the new sandbox [Activity] by
+ * calling [Activity#startActivity] against it.
+ * @param sdkActivityToken the identifier that is shared by the SDK which requests the
+ * [Activity].
+ * @see SdkSandboxManager.startSdkSandboxActivity
+ */
+ fun startSdkSandboxActivity(fromActivity: Activity, sdkActivityToken: IBinder) {
+ if (LocalSdkActivityStarter.tryStart(fromActivity, sdkActivityToken)) {
+ return
+ }
+ platformApi.startSdkSandboxActivity(fromActivity, sdkActivityToken)
+ }
+
@TestOnly
internal fun getLocallyLoadedSdk(sdkName: String): LocallyLoadedSdks.Entry? =
localLocallyLoadedSdks.get(sdkName)
@@ -228,6 +253,8 @@
@DoNotInline
fun getSandboxedSdks(): List<SandboxedSdkCompat> = emptyList()
+
+ fun startSdkSandboxActivity(fromActivity: Activity, sdkActivityToken: IBinder)
}
@RequiresApi(33)
@@ -284,6 +311,12 @@
}
}
+ override fun startSdkSandboxActivity(fromActivity: Activity, sdkActivityToken: IBinder) {
+ throw UnsupportedOperationException(
+ "This API is only supported for devices run on Android U+"
+ )
+ }
+
private suspend fun loadSdkInternal(
sdkName: String,
params: Bundle
@@ -310,7 +343,7 @@
@RequiresApi(33)
@RequiresExtension(extension = AD_SERVICES, version = 5)
- private class ApiAdServicesV5Impl(
+ private open class ApiAdServicesV5Impl(
context: Context
) : ApiAdServicesV4Impl(context) {
@DoNotInline
@@ -321,6 +354,16 @@
}
}
+ @RequiresExtension(extension = AD_SERVICES, version = 5)
+ @RequiresApi(34)
+ private class ApiAdServicesUDCImpl(
+ context: Context
+ ) : ApiAdServicesV5Impl(context) {
+ override fun startSdkSandboxActivity(fromActivity: Activity, sdkActivityToken: IBinder) {
+ sdkSandboxManager.startSdkSandboxActivity(fromActivity, sdkActivityToken)
+ }
+ }
+
private class FailImpl : PlatformApi {
@DoNotInline
override suspend fun loadSdk(
@@ -343,6 +386,9 @@
callback: SdkSandboxProcessDeathCallbackCompat
) {
}
+
+ override fun startSdkSandboxActivity(fromActivity: Activity, sdkActivityToken: IBinder) {
+ }
}
companion object {
@@ -389,8 +435,11 @@
private object PlatformApiFactory {
@SuppressLint("NewApi", "ClassVerificationFailure")
+ @OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
fun create(context: Context): PlatformApi {
- return if (AdServicesInfo.isAtLeastV5()) {
+ return if (BuildCompat.isAtLeastU()) {
+ ApiAdServicesUDCImpl(context)
+ } else if (AdServicesInfo.isAtLeastV5()) {
ApiAdServicesV5Impl(context)
} else if (AdServicesInfo.isAtLeastV4()) {
ApiAdServicesV4Impl(context)
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/activity/ComponentActivityHolder.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/activity/ComponentActivityHolder.kt
new file mode 100644
index 0000000..f47279e
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/activity/ComponentActivityHolder.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.activity
+
+import android.app.Activity
+import androidx.activity.ComponentActivity
+import androidx.activity.OnBackPressedDispatcher
+import androidx.lifecycle.Lifecycle
+import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
+
+/**
+ * Simple implementation of [ActivityHolder] for [ComponentActivity].
+ */
+internal class ComponentActivityHolder(
+ private val activity: ComponentActivity
+) : ActivityHolder {
+ override fun getActivity(): Activity = activity
+
+ override fun getOnBackPressedDispatcher(): OnBackPressedDispatcher =
+ activity.onBackPressedDispatcher
+
+ override val lifecycle: Lifecycle
+ get() = activity.lifecycle
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/activity/LocalSdkActivityHandlerRegistry.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/activity/LocalSdkActivityHandlerRegistry.kt
new file mode 100644
index 0000000..2f9bf53
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/activity/LocalSdkActivityHandlerRegistry.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.privacysandbox.sdkruntime.client.activity
+
+import android.os.Binder
+import android.os.IBinder
+import androidx.annotation.GuardedBy
+import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
+import org.jetbrains.annotations.TestOnly
+
+/**
+ * Singleton class to store instances of [SdkSandboxActivityHandlerCompat] registered by locally
+ * loaded SDKs.
+ */
+internal object LocalSdkActivityHandlerRegistry {
+
+ private val mapsLock = Any()
+
+ @GuardedBy("mapsLock")
+ private val handlerToToken =
+ hashMapOf<SdkSandboxActivityHandlerCompat, IBinder>()
+
+ @GuardedBy("mapsLock")
+ private val tokenToHandler =
+ hashMapOf<IBinder, SdkSandboxActivityHandlerCompat>()
+
+ fun register(handler: SdkSandboxActivityHandlerCompat): IBinder =
+ synchronized(mapsLock) {
+ val existingToken = handlerToToken[handler]
+ if (existingToken != null) {
+ return existingToken
+ }
+
+ val token = Binder()
+ handlerToToken[handler] = token
+ tokenToHandler[token] = handler
+
+ return token
+ }
+
+ fun unregister(handler: SdkSandboxActivityHandlerCompat) =
+ synchronized(mapsLock) {
+ val unregisteredToken = handlerToToken.remove(handler)
+ if (unregisteredToken != null) {
+ tokenToHandler.remove(unregisteredToken)
+ }
+ }
+
+ fun isRegistered(token: IBinder): Boolean =
+ synchronized(mapsLock) {
+ return tokenToHandler.containsKey(token)
+ }
+
+ @TestOnly
+ fun getHandlerByToken(token: IBinder): SdkSandboxActivityHandlerCompat? =
+ synchronized(mapsLock) {
+ return tokenToHandler[token]
+ }
+
+ fun notifyOnActivityCreation(
+ token: IBinder,
+ activityHolder: ActivityHolder
+ ) = synchronized(mapsLock) {
+ val handler = tokenToHandler[token]
+ ?: throw IllegalStateException("There is no registered handler to notify")
+ handler.onActivityCreated(activityHolder)
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/activity/LocalSdkActivityStarter.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/activity/LocalSdkActivityStarter.kt
new file mode 100644
index 0000000..64fc30a
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/activity/LocalSdkActivityStarter.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.activity
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import android.os.IBinder
+import androidx.core.os.BundleCompat
+
+/**
+ * Singleton helper object to start [SdkActivity].
+ * Creates [Intent] with token provided by SDK.
+ */
+internal object LocalSdkActivityStarter {
+
+ private const val EXTRA_ACTIVITY_TOKEN = "androidx.privacysandbox.sdkruntime.ACTIVITY_HANDLER"
+
+ /**
+ * Trying to start [SdkActivity].
+ *
+ * If [token] registered in [LocalSdkActivityHandlerRegistry] this method will create
+ * [Intent] for starting [SdkActivity] and call [Activity.startActivity]
+ *
+ * @param fromActivity the [Activity] will be used to start the new [SdkActivity] by
+ * calling [Activity.startActivity] against it.
+ * @param token the identifier that is shared by the SDK which requests the [Activity].
+ *
+ * @return true if Intent was created, false if not (token wasn't registered locally).
+ */
+ fun tryStart(fromActivity: Activity, token: IBinder): Boolean {
+ if (!LocalSdkActivityHandlerRegistry.isRegistered(token)) {
+ return false
+ }
+
+ val intent = Intent(fromActivity, SdkActivity::class.java)
+
+ val params = Bundle()
+ BundleCompat.putBinder(params, EXTRA_ACTIVITY_TOKEN, token)
+ intent.putExtras(params)
+
+ fromActivity.startActivity(intent)
+
+ return true
+ }
+
+ /**
+ * Retrieve token from [Intent] used for creation [SdkActivity].
+ *
+ * @return token or null if [EXTRA_ACTIVITY_TOKEN] param is missing in [Intent.getExtras]
+ */
+ fun getTokenFromSdkActivityStartIntent(intent: Intent): IBinder? {
+ val params = intent.extras ?: return null
+ return BundleCompat.getBinder(params, EXTRA_ACTIVITY_TOKEN)
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/activity/SdkActivity.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/activity/SdkActivity.kt
new file mode 100644
index 0000000..9c25338
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/activity/SdkActivity.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.activity
+
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat
+import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
+
+/**
+ * Activity to start for SDKs running locally.
+ * Not for App / SDK Usage.
+ *
+ * SDK should use [SdkSandboxControllerCompat.registerSdkSandboxActivityHandler] for handler
+ * registration.
+ *
+ * App should use [SdkSandboxManagerCompat.startSdkSandboxActivity] for starting activity.
+ */
+class SdkActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ notifySdkOnActivityCreation()
+ }
+
+ private fun notifySdkOnActivityCreation() {
+ val token = LocalSdkActivityStarter.getTokenFromSdkActivityStartIntent(intent)
+ if (token == null) {
+ Log.e(
+ LOG_TAG,
+ "Token is missing in starting SdkActivity intent params"
+ )
+ finish()
+ return
+ }
+
+ try {
+ val activityHolder = ComponentActivityHolder(this)
+ LocalSdkActivityHandlerRegistry.notifyOnActivityCreation(token, activityHolder)
+ } catch (e: Exception) {
+ Log.e(
+ LOG_TAG,
+ "Failed to start the SdkActivity and going to finish it: ",
+ e
+ )
+ finish()
+ }
+ }
+
+ private companion object {
+ private const val LOG_TAG = "SdkActivity"
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/controller/LocalController.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/controller/LocalController.kt
index ecddba5..7ad3265 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/controller/LocalController.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/controller/LocalController.kt
@@ -16,7 +16,10 @@
package androidx.privacysandbox.sdkruntime.client.controller
+import android.os.IBinder
+import androidx.privacysandbox.sdkruntime.client.activity.LocalSdkActivityHandlerRegistry
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
/**
@@ -29,4 +32,16 @@
override fun getSandboxedSdks(): List<SandboxedSdkCompat> {
return locallyLoadedSdks.getLoadedSdks()
}
+
+ override fun registerSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ): IBinder {
+ return LocalSdkActivityHandlerRegistry.register(handlerCompat)
+ }
+
+ override fun unregisterSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ) {
+ LocalSdkActivityHandlerRegistry.unregister(handlerCompat)
+ }
}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoader.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoader.kt
index 3fcefd8..f8b08ed 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoader.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoader.kt
@@ -64,7 +64,7 @@
val apiVersion = VersionHandshake.perform(classLoader)
ResourceRemapping.apply(classLoader, sdkConfig.resourceRemapping)
if (apiVersion >= 2) {
- return createSdkProviderV2(classLoader, sdkConfig)
+ return createSdkProviderV2(classLoader, apiVersion, sdkConfig)
} else if (apiVersion >= 1) {
return createSdkProviderV1(classLoader, sdkConfig)
}
@@ -91,9 +91,10 @@
private fun createSdkProviderV2(
sdkClassLoader: ClassLoader,
+ sdkVersion: Int,
sdkConfig: LocalSdkConfig
): LocalSdkProvider {
- SandboxControllerInjector.inject(sdkClassLoader, controller)
+ SandboxControllerInjector.inject(sdkClassLoader, sdkVersion, controller)
return SdkProviderV1.create(sdkClassLoader, sdkConfig, appContext)
}
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SandboxControllerInjector.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SandboxControllerInjector.kt
index 745c352..b313355 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SandboxControllerInjector.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SandboxControllerInjector.kt
@@ -16,9 +16,12 @@
package androidx.privacysandbox.sdkruntime.client.loader.impl
+import android.annotation.SuppressLint
import android.os.IBinder
+import androidx.privacysandbox.sdkruntime.client.loader.impl.injector.SdkActivityHandlerWrapper
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkInfo
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
import java.lang.reflect.Constructor
import java.lang.reflect.InvocationHandler
@@ -38,8 +41,10 @@
* 2) Create proxy that implements class from (1) and delegate to [controller]
* 3) Call (via reflection) [SdkSandboxControllerCompat.injectLocalImpl] with proxy from (2)
*/
+ @SuppressLint("BanUncheckedReflection") // using reflection on library classes
fun inject(
sdkClassLoader: ClassLoader,
+ sdkVersion: Int,
controller: SdkSandboxControllerCompat.SandboxControllerImpl
) {
val controllerClass = Class.forName(
@@ -58,12 +63,18 @@
val sdkCompatBuilder = CompatSdkBuilder.createFor(sdkClassLoader)
+ val sdkActivityHandlerWrapper = if (sdkVersion >= 3)
+ SdkActivityHandlerWrapper.createFor(sdkClassLoader)
+ else
+ null
+
val proxy = Proxy.newProxyInstance(
sdkClassLoader,
arrayOf(controllerImplClass),
Handler(
controller,
- sdkCompatBuilder
+ sdkCompatBuilder,
+ sdkActivityHandlerWrapper
)
)
@@ -72,11 +83,23 @@
private class Handler(
private val controller: SdkSandboxControllerCompat.SandboxControllerImpl,
- private val compatSdkBuilder: CompatSdkBuilder
+ private val compatSdkBuilder: CompatSdkBuilder,
+ private val sdkActivityHandlerWrapper: SdkActivityHandlerWrapper?
) : InvocationHandler {
+
+ private val sdkToAppHandlerMap =
+ hashMapOf<Any, SdkSandboxActivityHandlerCompat>()
+
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any {
return when (method.name) {
"getSandboxedSdks" -> getSandboxedSdks()
+
+ "registerSdkSandboxActivityHandler" ->
+ registerSdkSandboxActivityHandler(args!![0]!!)
+
+ "unregisterSdkSandboxActivityHandler" ->
+ unregisterSdkSandboxActivityHandler(args!![0]!!)
+
else -> {
throw UnsupportedOperationException(
"Unexpected method call object:$proxy, method: $method, args: $args"
@@ -90,6 +113,43 @@
.getSandboxedSdks()
.map { compatSdkBuilder.createFrom(it) }
}
+
+ private fun registerSdkSandboxActivityHandler(sdkSideHandler: Any): Any {
+ val handlerToRegister = wrapSdkActivityHandler(sdkSideHandler)
+ return controller
+ .registerSdkSandboxActivityHandler(handlerToRegister)
+ }
+
+ private fun unregisterSdkSandboxActivityHandler(sdkSideHandler: Any) {
+ val appSideHandler = synchronized(sdkToAppHandlerMap) {
+ sdkToAppHandlerMap.remove(sdkSideHandler)
+ }
+ if (appSideHandler != null) {
+ controller
+ .unregisterSdkSandboxActivityHandler(appSideHandler)
+ }
+ }
+
+ private fun wrapSdkActivityHandler(sdkSideHandler: Any): SdkSandboxActivityHandlerCompat =
+ synchronized(sdkToAppHandlerMap) {
+ if (sdkActivityHandlerWrapper == null) {
+ throw IllegalStateException(
+ "Unexpected call from SDK without Activity support"
+ )
+ }
+
+ val existingAppSideHandler = sdkToAppHandlerMap[sdkSideHandler]
+ if (existingAppSideHandler != null) {
+ return existingAppSideHandler
+ }
+
+ val appSideHandler =
+ sdkActivityHandlerWrapper.wrapSdkSandboxActivityHandlerCompat(sdkSideHandler)
+
+ sdkToAppHandlerMap[sdkSideHandler] = appSideHandler
+
+ return appSideHandler
+ }
}
private class CompatSdkBuilder(
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/ActivityHolderProxyFactory.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/ActivityHolderProxyFactory.kt
new file mode 100644
index 0000000..1ab51be
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/ActivityHolderProxyFactory.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader.impl.injector
+
+import android.app.Activity
+import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
+import java.lang.reflect.Constructor
+import java.lang.reflect.InvocationHandler
+import java.lang.reflect.Method
+import java.lang.reflect.Proxy
+
+/**
+ * Create proxy of [ActivityHolder] that implements same interface loaded by SDK Classloader.
+ */
+internal class ActivityHolderProxyFactory private constructor(
+ private val sdkClassLoader: ClassLoader,
+
+ private val activityHolderClass: Class<*>,
+
+ private val onBackPressedDispatcherConstructor: Constructor<out Any>,
+
+ private val lifecycleRegistryConstructor: Constructor<out Any>,
+) {
+
+ fun createProxyFor(activityHolder: ActivityHolder): Any {
+ val dispatcherProxy = setupOnBackInvokedDispatcherProxy()
+
+ val handler = ActivityHolderHandler(
+ activityHolder.getActivity(),
+ dispatcherProxy
+ )
+
+ val activityHolderProxy = Proxy.newProxyInstance(
+ sdkClassLoader,
+ arrayOf(activityHolderClass),
+ handler
+ )
+
+ val lifecycleProxy = setupLifecycleProxy(activityHolderProxy)
+ handler.lifecycleProxy = lifecycleProxy
+
+ return activityHolderProxy
+ }
+
+ private fun setupOnBackInvokedDispatcherProxy(): Any {
+ // TODO (b/280783465) Proxy back events from original dispatcher to proxy
+ return onBackPressedDispatcherConstructor.newInstance()
+ }
+
+ private fun setupLifecycleProxy(
+ activityHolderProxy: Any
+ ): Any {
+ // TODO (b/280783461) Proxy lifecycle events from original lifecycle to proxy
+ return lifecycleRegistryConstructor.newInstance(activityHolderProxy)
+ }
+
+ private class ActivityHolderHandler(
+ private val activity: Activity,
+ private val onBackPressedDispatcherProxy: Any,
+ ) : InvocationHandler {
+
+ var lifecycleProxy: Any? = null
+
+ override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any {
+ return when (method.name) {
+ "equals" -> proxy === args?.get(0)
+ "hashCode" -> hashCode()
+ "toString" -> toString()
+ "getActivity" -> activity
+ "getOnBackPressedDispatcher" -> onBackPressedDispatcherProxy
+ "getLifecycle" -> lifecycleProxy!!
+ else -> {
+ throw UnsupportedOperationException(
+ "Unexpected method call object:$proxy, method: $method, args: $args"
+ )
+ }
+ }
+ }
+ }
+
+ companion object {
+ fun createFor(classLoader: ClassLoader): ActivityHolderProxyFactory {
+ val activityHolderClass = Class.forName(
+ ActivityHolder::class.java.name,
+ /* initialize = */ false,
+ classLoader
+ )
+ val onBackPressedDispatcherClass = Class.forName(
+ "androidx.activity.OnBackPressedDispatcher",
+ /* initialize = */ false,
+ classLoader
+ )
+ val lifecycleOwnerClass = Class.forName(
+ "androidx.lifecycle.LifecycleOwner",
+ /* initialize = */ false,
+ classLoader
+ )
+ val lifecycleRegistryClass = Class.forName(
+ "androidx.lifecycle.LifecycleRegistry",
+ /* initialize = */ false,
+ classLoader
+ )
+
+ val onBackPressedDispatcherConstructor =
+ onBackPressedDispatcherClass.getConstructor()
+
+ val lifecycleRegistryConstructor =
+ lifecycleRegistryClass.getConstructor(
+ /* parameter1 */ lifecycleOwnerClass
+ )
+
+ return ActivityHolderProxyFactory(
+ sdkClassLoader = classLoader,
+ activityHolderClass = activityHolderClass,
+ onBackPressedDispatcherConstructor = onBackPressedDispatcherConstructor,
+ lifecycleRegistryConstructor = lifecycleRegistryConstructor,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/SdkActivityHandlerWrapper.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/SdkActivityHandlerWrapper.kt
new file mode 100644
index 0000000..d9549f9
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/SdkActivityHandlerWrapper.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader.impl.injector
+
+import android.annotation.SuppressLint
+import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
+import java.lang.reflect.Method
+
+/**
+ * Creates reflection wrapper for implementation of [SdkSandboxActivityHandlerCompat] interface
+ * loaded by SDK classloader.
+ */
+internal class SdkActivityHandlerWrapper private constructor(
+ private val activityHolderProxyFactory: ActivityHolderProxyFactory,
+ private val handlerOnActivityCreatedMethod: Method,
+) {
+
+ fun wrapSdkSandboxActivityHandlerCompat(handlerCompat: Any): SdkSandboxActivityHandlerCompat =
+ WrappedHandler(handlerCompat, handlerOnActivityCreatedMethod, activityHolderProxyFactory)
+
+ private class WrappedHandler(
+ private val originalHandler: Any,
+ private val handlerOnActivityCreatedMethod: Method,
+ private val activityHolderProxyFactory: ActivityHolderProxyFactory
+ ) : SdkSandboxActivityHandlerCompat {
+
+ @SuppressLint("BanUncheckedReflection") // using reflection on library classes
+ override fun onActivityCreated(activityHolder: ActivityHolder) {
+ val activityHolderProxy = activityHolderProxyFactory.createProxyFor(activityHolder)
+ handlerOnActivityCreatedMethod.invoke(originalHandler, activityHolderProxy)
+ }
+ }
+
+ companion object {
+ fun createFor(classLoader: ClassLoader): SdkActivityHandlerWrapper {
+ val sdkSandboxActivityHandlerCompatClass = Class.forName(
+ SdkSandboxActivityHandlerCompat::class.java.name,
+ /* initialize = */ false,
+ classLoader
+ )
+ val activityHolderClass = Class.forName(
+ ActivityHolder::class.java.name,
+ /* initialize = */ false,
+ classLoader
+ )
+ val handlerOnActivityCreatedMethod =
+ sdkSandboxActivityHandlerCompatClass.getMethod(
+ "onActivityCreated",
+ activityHolderClass
+ )
+
+ val activityHolderProxyFactory = ActivityHolderProxyFactory.createFor(classLoader)
+
+ return SdkActivityHandlerWrapper(
+ activityHolderProxyFactory = activityHolderProxyFactory,
+ handlerOnActivityCreatedMethod = handlerOnActivityCreatedMethod
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/api/current.txt b/privacysandbox/sdkruntime/sdkruntime-core/api/current.txt
index 56a2116..0d1578a 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/api/current.txt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/api/current.txt
@@ -51,11 +51,26 @@
}
+package androidx.privacysandbox.sdkruntime.core.activity {
+
+ public interface ActivityHolder extends androidx.lifecycle.LifecycleOwner {
+ method public android.app.Activity getActivity();
+ method public androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
+ }
+
+ public interface SdkSandboxActivityHandlerCompat {
+ method public void onActivityCreated(androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder activityHolder);
+ }
+
+}
+
package androidx.privacysandbox.sdkruntime.core.controller {
public final class SdkSandboxControllerCompat {
method public static androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat from(android.content.Context context);
method public java.util.List<androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat> getSandboxedSdks();
+ method public android.os.IBinder registerSdkSandboxActivityHandler(androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat handlerCompat);
+ method public void unregisterSdkSandboxActivityHandler(androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat handlerCompat);
field public static final androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat.Companion Companion;
}
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/api/public_plus_experimental_current.txt b/privacysandbox/sdkruntime/sdkruntime-core/api/public_plus_experimental_current.txt
index 56a2116..0d1578a 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/api/public_plus_experimental_current.txt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/api/public_plus_experimental_current.txt
@@ -51,11 +51,26 @@
}
+package androidx.privacysandbox.sdkruntime.core.activity {
+
+ public interface ActivityHolder extends androidx.lifecycle.LifecycleOwner {
+ method public android.app.Activity getActivity();
+ method public androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
+ }
+
+ public interface SdkSandboxActivityHandlerCompat {
+ method public void onActivityCreated(androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder activityHolder);
+ }
+
+}
+
package androidx.privacysandbox.sdkruntime.core.controller {
public final class SdkSandboxControllerCompat {
method public static androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat from(android.content.Context context);
method public java.util.List<androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat> getSandboxedSdks();
+ method public android.os.IBinder registerSdkSandboxActivityHandler(androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat handlerCompat);
+ method public void unregisterSdkSandboxActivityHandler(androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat handlerCompat);
field public static final androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat.Companion Companion;
}
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/api/restricted_current.txt b/privacysandbox/sdkruntime/sdkruntime-core/api/restricted_current.txt
index 56a2116..0d1578a 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/api/restricted_current.txt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/api/restricted_current.txt
@@ -51,11 +51,26 @@
}
+package androidx.privacysandbox.sdkruntime.core.activity {
+
+ public interface ActivityHolder extends androidx.lifecycle.LifecycleOwner {
+ method public android.app.Activity getActivity();
+ method public androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
+ }
+
+ public interface SdkSandboxActivityHandlerCompat {
+ method public void onActivityCreated(androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder activityHolder);
+ }
+
+}
+
package androidx.privacysandbox.sdkruntime.core.controller {
public final class SdkSandboxControllerCompat {
method public static androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat from(android.content.Context context);
method public java.util.List<androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat> getSandboxedSdks();
+ method public android.os.IBinder registerSdkSandboxActivityHandler(androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat handlerCompat);
+ method public void unregisterSdkSandboxActivityHandler(androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat handlerCompat);
field public static final androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat.Companion Companion;
}
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/build.gradle b/privacysandbox/sdkruntime/sdkruntime-core/build.gradle
index 68c7464..ca4567e 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/build.gradle
+++ b/privacysandbox/sdkruntime/sdkruntime-core/build.gradle
@@ -26,7 +26,8 @@
api(libs.kotlinStdlib)
api("androidx.annotation:annotation:1.6.0")
- implementation("androidx.core:core:1.8.0")
+ implementation("androidx.core:core:1.12.0-alpha03")
+ implementation(projectOrArtifact(":activity:activity"))
// TODO(b/249982004): cleanup dependencies
androidTestImplementation(libs.testCore)
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/lint-baseline.xml b/privacysandbox/sdkruntime/sdkruntime-core/lint-baseline.xml
new file mode 100644
index 0000000..4f913f5e
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/lint-baseline.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.1.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta01)" variant="all" version="8.1.0-beta01">
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompat.kt"/>
+ </issue>
+
+</issues>
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatLocalTest.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatLocalTest.kt
index 1307cec..7d96300 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatLocalTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatLocalTest.kt
@@ -18,13 +18,17 @@
import android.content.Context
import android.os.Binder
+import android.os.IBinder
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
import androidx.privacysandbox.sdkruntime.core.Versions
+import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.After
+import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -71,9 +75,85 @@
assertThat(sandboxedSdks).isEqualTo(expectedResult)
}
- private class TestStubImpl(
+ @Test
+ fun registerSdkSandboxActivityHandler_withLocalImpl_registerItInLocalImpl() {
+ val localImpl = TestStubImpl()
+ SdkSandboxControllerCompat.injectLocalImpl(localImpl)
+
+ val controllerCompat = SdkSandboxControllerCompat.from(context)
+ val handlerCompat = object : SdkSandboxActivityHandlerCompat {
+ override fun onActivityCreated(activityHolder: ActivityHolder) {}
+ }
+ val token = controllerCompat.registerSdkSandboxActivityHandler(handlerCompat)
+ assertThat(token).isEqualTo(localImpl.token)
+ }
+
+ @Test
+ fun unregisterSdkSandboxActivityHandler_withLocalImpl_unregisterItFromLocalImpl() {
+ val localImpl = TestStubImpl()
+ SdkSandboxControllerCompat.injectLocalImpl(localImpl)
+
+ val controllerCompat = SdkSandboxControllerCompat.from(context)
+ val handlerCompat = object : SdkSandboxActivityHandlerCompat {
+ override fun onActivityCreated(activityHolder: ActivityHolder) {}
+ }
+ val token = controllerCompat.registerSdkSandboxActivityHandler(handlerCompat)
+ assertThat(token).isEqualTo(localImpl.token)
+
+ controllerCompat.unregisterSdkSandboxActivityHandler(handlerCompat)
+ assertThat(localImpl.token).isNull()
+ }
+
+ @Test
+ fun registerSdkSandboxActivityHandler_clientApiBelow3_throwsUnsupportedOperationException() {
+ // Emulate loading via client lib with version below 3
+ Versions.handShake(2)
+
+ SdkSandboxControllerCompat.injectLocalImpl(TestStubImpl())
+ val controllerCompat = SdkSandboxControllerCompat.from(context)
+
+ Assert.assertThrows(UnsupportedOperationException::class.java) {
+ controllerCompat.registerSdkSandboxActivityHandler(
+ object : SdkSandboxActivityHandlerCompat {
+ override fun onActivityCreated(activityHolder: ActivityHolder) {}
+ }
+ )
+ }
+ }
+
+ @Test
+ fun unregisterSdkSandboxActivityHandler_clientApiBelow3_throwsUnsupportedOperationException() {
+ // Emulate loading via client lib with version below 3
+ Versions.handShake(2)
+
+ SdkSandboxControllerCompat.injectLocalImpl(TestStubImpl())
+ val controllerCompat = SdkSandboxControllerCompat.from(context)
+
+ Assert.assertThrows(UnsupportedOperationException::class.java) {
+ controllerCompat.unregisterSdkSandboxActivityHandler(
+ object : SdkSandboxActivityHandlerCompat {
+ override fun onActivityCreated(activityHolder: ActivityHolder) {}
+ }
+ )
+ }
+ }
+
+ internal class TestStubImpl(
private val sandboxedSdks: List<SandboxedSdkCompat> = emptyList()
) : SdkSandboxControllerCompat.SandboxControllerImpl {
+ var token: IBinder? = null
override fun getSandboxedSdks() = sandboxedSdks
+ override fun registerSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ): IBinder {
+ token = Binder()
+ return token!!
+ }
+
+ override fun unregisterSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ) {
+ token = null
+ }
}
}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatSandboxedTest.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatSandboxedTest.kt
index b133fe0..bcac996 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatSandboxedTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatSandboxedTest.kt
@@ -16,23 +16,35 @@
package androidx.privacysandbox.sdkruntime.core.controller
+import android.app.Activity
+import android.app.Application
import android.app.sdksandbox.SandboxedSdk
+import android.app.sdksandbox.sdkprovider.SdkSandboxActivityHandler
import android.app.sdksandbox.sdkprovider.SdkSandboxController
import android.content.Context
import android.os.Binder
import android.os.Build
+import android.os.Bundle
import android.os.ext.SdkExtensions
+import android.window.OnBackInvokedDispatcher
import androidx.annotation.RequiresExtension
+import androidx.lifecycle.Lifecycle
import androidx.privacysandbox.sdkruntime.core.AdServicesInfo
+import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SdkSuppress
+import androidx.test.internal.runner.junit4.statement.UiThreadStatement
import com.google.common.truth.Truth.assertThat
+import org.junit.Assert
import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeTrue
import org.junit.Test
+import org.mockito.ArgumentCaptor
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.Mockito.`when`
@@ -41,7 +53,7 @@
class SdkSandboxControllerCompatSandboxedTest {
@Test
- fun getSandboxedSdks_whenApiNotAvailable_notDelegateToSandbox() {
+ fun controllerAPIs_whenApiNotAvailable_notDelegateToSandbox() {
assumeFalse(
"Requires SandboxController API not available",
isSandboxControllerAvailable()
@@ -51,7 +63,15 @@
val controllerCompat = SdkSandboxControllerCompat.from(context)
controllerCompat.getSandboxedSdks()
-
+ val handlerCompat = object : SdkSandboxActivityHandlerCompat {
+ override fun onActivityCreated(activityHolder: ActivityHolder) {}
+ }
+ Assert.assertThrows(UnsupportedOperationException::class.java) {
+ controllerCompat.registerSdkSandboxActivityHandler(handlerCompat)
+ }
+ Assert.assertThrows(UnsupportedOperationException::class.java) {
+ controllerCompat.unregisterSdkSandboxActivityHandler(handlerCompat)
+ }
verifyZeroInteractions(context)
}
@@ -97,6 +117,120 @@
assertThat(result.getInterface()).isEqualTo(sandboxedSdk.getInterface())
}
+ @Test
+ // TODO(b/262577044) Remove RequiresExtension after extensions support in @SdkSuppress
+ @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+ fun registerSdkSandboxHandlerCompat_whenApiAvailable_registerItToPlatform() {
+ val context = spy(ApplicationProvider.getApplicationContext<Context>())
+ val sdkSandboxController = mock(SdkSandboxController::class.java)
+ doReturn(sdkSandboxController)
+ .`when`(context).getSystemService(SdkSandboxController::class.java)
+
+ val platformRegisteredHandlerCaptor = ArgumentCaptor.forClass(
+ SdkSandboxActivityHandler::class.java)
+ doReturn(Binder())
+ .`when`(sdkSandboxController).registerSdkSandboxActivityHandler(
+ platformRegisteredHandlerCaptor.capture())
+
+ val handlerCompat = mock(SdkSandboxActivityHandlerCompat::class.java)
+ val controllerCompat = SdkSandboxControllerCompat.from(context)
+ controllerCompat.registerSdkSandboxActivityHandler(handlerCompat)
+
+ verify(sdkSandboxController).registerSdkSandboxActivityHandler(
+ platformRegisteredHandlerCaptor.value
+ )
+
+ val activityMock = mock(Activity::class.java)
+ val onBackInvokedDispatcher = mock(OnBackInvokedDispatcher::class.java)
+ doReturn(onBackInvokedDispatcher).`when`(activityMock).onBackInvokedDispatcher
+
+ platformRegisteredHandlerCaptor.value.onActivityCreated(activityMock)
+ var activityHolderCaptor: ArgumentCaptor<ActivityHolder> =
+ ArgumentCaptor.forClass(ActivityHolder::class.java)
+ verify(handlerCompat).onActivityCreated(capture(activityHolderCaptor))
+ assertThat(activityHolderCaptor.value.getActivity()).isEqualTo(activityMock)
+
+ assertThat(activityHolderCaptor.value.getOnBackPressedDispatcher()).isNotNull()
+
+ assertThat(activityHolderCaptor.value.lifecycle).isNotNull()
+ var activityLifecycleCallbackCaptor:
+ ArgumentCaptor<Application.ActivityLifecycleCallbacks> =
+ ArgumentCaptor.forClass(Application.ActivityLifecycleCallbacks::class.java)
+ verify(activityMock).registerActivityLifecycleCallbacks(
+ activityLifecycleCallbackCaptor.capture()
+ )
+ var bundleMock = mock(Bundle::class.java)
+ UiThreadStatement.runOnUiThread {
+ assertThat(activityHolderCaptor.value.lifecycle.currentState).isEqualTo(
+ Lifecycle.State.INITIALIZED)
+ activityLifecycleCallbackCaptor.value.onActivityCreated(activityMock, bundleMock)
+ assertThat(activityHolderCaptor.value.lifecycle.currentState).isEqualTo(
+ Lifecycle.State.CREATED)
+
+ activityLifecycleCallbackCaptor.value.onActivityStarted(activityMock)
+ assertThat(activityHolderCaptor.value.lifecycle.currentState).isEqualTo(
+ Lifecycle.State.STARTED)
+
+ activityLifecycleCallbackCaptor.value.onActivityResumed(activityMock)
+ assertThat(activityHolderCaptor.value.lifecycle.currentState).isEqualTo(
+ Lifecycle.State.RESUMED)
+
+ activityLifecycleCallbackCaptor.value.onActivityPaused(activityMock)
+ assertThat(activityHolderCaptor.value.lifecycle.currentState).isEqualTo(
+ Lifecycle.State.STARTED)
+
+ activityLifecycleCallbackCaptor.value.onActivityStopped(activityMock)
+ assertThat(activityHolderCaptor.value.lifecycle.currentState).isEqualTo(
+ Lifecycle.State.CREATED)
+
+ activityLifecycleCallbackCaptor.value.onActivityDestroyed(activityMock)
+ assertThat(activityHolderCaptor.value.lifecycle.currentState).isEqualTo(
+ Lifecycle.State.DESTROYED)
+
+ val currentState = activityHolderCaptor.value.lifecycle.currentState
+ activityLifecycleCallbackCaptor.value.onActivitySaveInstanceState(
+ activityMock, bundleMock)
+ assertThat(activityHolderCaptor.value.lifecycle.currentState).isEqualTo(currentState)
+ }
+ }
+
+ @Test
+ // TODO(b/262577044) Remove RequiresExtension after extensions support in @SdkSuppress
+ @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+ fun unregisterSdkSandboxHandlerCompat_whenApiAvailable_unregisterItToPlatform() {
+ val context = spy(ApplicationProvider.getApplicationContext<Context>())
+ val sdkSandboxController = mock(SdkSandboxController::class.java)
+ doReturn(sdkSandboxController)
+ .`when`(context).getSystemService(SdkSandboxController::class.java)
+
+ val registeredHandlerCaptor = ArgumentCaptor.forClass(SdkSandboxActivityHandler::class.java)
+ doReturn(Binder())
+ .`when`(sdkSandboxController).registerSdkSandboxActivityHandler(
+ registeredHandlerCaptor.capture())
+
+ val controllerCompat = SdkSandboxControllerCompat.from(context)
+ val handlerCompat = mock(SdkSandboxActivityHandlerCompat::class.java)
+
+ controllerCompat.registerSdkSandboxActivityHandler(handlerCompat)
+ verify(sdkSandboxController).registerSdkSandboxActivityHandler(
+ registeredHandlerCaptor.value)
+
+ val unregisteredHandlerCaptor = ArgumentCaptor.forClass(
+ SdkSandboxActivityHandler::class.java
+ )
+ controllerCompat.unregisterSdkSandboxActivityHandler(handlerCompat)
+ verify(sdkSandboxController).unregisterSdkSandboxActivityHandler(
+ unregisteredHandlerCaptor.capture())
+
+ assertThat(unregisteredHandlerCaptor.value).isEqualTo(registeredHandlerCaptor.value)
+ }
+
private fun isSandboxControllerAvailable() =
AdServicesInfo.isAtLeastV5()
+
+ // To capture non null arguments.
+
+ private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
}
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/Versions.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/Versions.kt
index 18585d0..9ba440d 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/Versions.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/Versions.kt
@@ -31,7 +31,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
object Versions {
- const val API_VERSION = 2
+ const val API_VERSION = 3
@JvmField
var CLIENT_VERSION: Int? = null
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/activity/ActivityHolder.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/activity/ActivityHolder.kt
new file mode 100644
index 0000000..8832f0e
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/activity/ActivityHolder.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.privacysandbox.sdkruntime.core.activity
+
+import android.app.Activity
+import androidx.activity.OnBackPressedDispatcher
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+
+/**
+ * A holder for the [Activity] created for SDK.
+ *
+ * This is passed to SDKs through [SdkSandboxActivityHandlerCompat.onActivityCreated] to notify SDKs
+ * about the created [Activity].
+ *
+ * SDK can add [LifecycleObserver]s into it to observe the [Activity] lifecycle state.
+ */
+interface ActivityHolder : LifecycleOwner {
+ /**
+ * The [Activity] created for SDK.
+ */
+ fun getActivity(): Activity
+
+ /**
+ * The [OnBackPressedDispatcher] for the created [Activity].
+ */
+ fun getOnBackPressedDispatcher(): OnBackPressedDispatcher
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/activity/SdkSandboxActivityHandlerCompat.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/activity/SdkSandboxActivityHandlerCompat.kt
new file mode 100644
index 0000000..b41250a
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/activity/SdkSandboxActivityHandlerCompat.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.privacysandbox.sdkruntime.core.activity
+
+import android.app.Activity
+import android.app.sdksandbox.sdkprovider.SdkSandboxActivityHandler
+import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
+
+/**
+ * This is used to notify the SDK when an [Activity] is created for it.
+ *
+ * When an SDK wants to start an [Activity], it should register an implementation of this class by
+ * calling [SdkSandboxControllerCompat.registerSdkSandboxActivityHandler] that will return an
+ * [android.os.Binder] identifier for the registered [SdkSandboxControllerCompat].
+ *
+ * The SDK should be notified about the [Activity] creation through calling
+ * [SdkSandboxActivityHandlerCompat.onActivityCreated] which happens when the caller app calls
+ * `SdkSandboxManagerCompat#startSdkSandboxActivity(Activity, IBinder)` using the same
+ * [android.os.IBinder] identifier for the registered [SdkSandboxActivityHandlerCompat].
+ *
+ * @see SdkSandboxActivityHandler
+ */
+interface SdkSandboxActivityHandlerCompat {
+
+ /**
+ * Notifies SDK when an [Activity] gets created.
+ *
+ * This function is called synchronously from the main thread of the [Activity] that is getting
+ * created.
+ *
+ * SDK is expected to call [Activity.setContentView] to the passed [Activity] object to populate
+ * the view.
+ *
+ * @param activityHolder the [ActivityHolder] which holds the [Activity] which gets created
+ * @see SdkSandboxActivityHandler.onActivityCreated
+ */
+ fun onActivityCreated(activityHolder: ActivityHolder)
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompat.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompat.kt
index 9575052..5f3ff3b 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompat.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompat.kt
@@ -16,16 +16,23 @@
package androidx.privacysandbox.sdkruntime.core.controller
+import android.app.sdksandbox.sdkprovider.SdkSandboxController
import android.content.Context
+import android.os.IBinder
import androidx.annotation.Keep
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+import androidx.annotation.OptIn
+import androidx.core.os.BuildCompat
import androidx.privacysandbox.sdkruntime.core.AdServicesInfo
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
import androidx.privacysandbox.sdkruntime.core.Versions
+import androidx.privacysandbox.sdkruntime.core.controller.impl.LocalImpl
import androidx.privacysandbox.sdkruntime.core.controller.impl.NoOpImpl
import androidx.privacysandbox.sdkruntime.core.controller.impl.PlatformImpl
+import androidx.privacysandbox.sdkruntime.core.controller.impl.PlatformUDCImpl
import org.jetbrains.annotations.TestOnly
/**
@@ -56,10 +63,46 @@
fun getSandboxedSdks(): List<SandboxedSdkCompat> =
controllerImpl.getSandboxedSdks()
+ /**
+ * Returns an identifier for a [SdkSandboxActivityHandlerCompat] after registering it.
+ *
+ * This function registers an implementation of [SdkSandboxActivityHandlerCompat] created by
+ * an SDK and returns an [IBinder] which uniquely identifies the passed
+ * [SdkSandboxActivityHandlerCompat] object.
+ *
+ * @param handlerCompat is the [SdkSandboxActivityHandlerCompat] to register
+ * @return [IBinder] uniquely identify the passed [SdkSandboxActivityHandlerCompat]
+ * @see SdkSandboxController.registerSdkSandboxActivityHandler
+ */
+ fun registerSdkSandboxActivityHandler(handlerCompat: SdkSandboxActivityHandlerCompat):
+ IBinder = controllerImpl.registerSdkSandboxActivityHandler(handlerCompat)
+
+ /**
+ * Unregister an already registered [SdkSandboxActivityHandlerCompat].
+ *
+ * If the passed [SdkSandboxActivityHandlerCompat] is registered, it will be unregistered.
+ * Otherwise, it will do nothing.
+ *
+ * If the [IBinder] token of the unregistered handler used to start a [android.app.Activity],
+ * the [android.app.Activity] will fail to start.
+ *
+ * @param handlerCompat is the [SdkSandboxActivityHandlerCompat] to unregister.
+ * @see SdkSandboxController.unregisterSdkSandboxActivityHandler
+ */
+ fun unregisterSdkSandboxActivityHandler(handlerCompat: SdkSandboxActivityHandlerCompat) =
+ controllerImpl.unregisterSdkSandboxActivityHandler(handlerCompat)
+
/** @suppress */
@RestrictTo(LIBRARY_GROUP)
interface SandboxControllerImpl {
fun getSandboxedSdks(): List<SandboxedSdkCompat>
+
+ fun registerSdkSandboxActivityHandler(handlerCompat: SdkSandboxActivityHandlerCompat):
+ IBinder
+
+ fun unregisterSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ )
}
companion object {
@@ -75,20 +118,16 @@
*/
@JvmStatic
fun from(context: Context): SdkSandboxControllerCompat {
- val loadedLocally = Versions.CLIENT_VERSION != null
- if (loadedLocally) {
+ val clientVersion = Versions.CLIENT_VERSION
+ if (clientVersion != null) {
val implFromClient = localImpl
if (implFromClient != null) {
- return SdkSandboxControllerCompat(implFromClient)
+ return SdkSandboxControllerCompat(LocalImpl(implFromClient, clientVersion))
}
return SdkSandboxControllerCompat(NoOpImpl())
}
-
- if (AdServicesInfo.isAtLeastV5()) {
- return SdkSandboxControllerCompat(PlatformImpl.from(context))
- }
-
- return SdkSandboxControllerCompat(NoOpImpl())
+ val platformImpl = PlatformImplFactory.create(context)
+ return SdkSandboxControllerCompat(platformImpl)
}
/**
@@ -112,4 +151,17 @@
localImpl = null
}
}
+
+ private object PlatformImplFactory {
+ @OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
+ fun create(context: Context): SandboxControllerImpl {
+ if (AdServicesInfo.isAtLeastV5()) {
+ if (BuildCompat.isAtLeastU()) {
+ return PlatformUDCImpl.from(context)
+ }
+ return PlatformImpl.from(context)
+ }
+ return NoOpImpl()
+ }
+ }
}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/LocalImpl.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/LocalImpl.kt
new file mode 100644
index 0000000..935814d
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/LocalImpl.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.privacysandbox.sdkruntime.core.controller.impl
+
+import android.os.IBinder
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
+import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
+
+/**
+ * Wrapper for client provided implementation of [SdkSandboxControllerCompat].
+ * Checks client version to determine if method supported.
+ */
+internal class LocalImpl(
+ private val implFromClient: SdkSandboxControllerCompat.SandboxControllerImpl,
+ private val clientVersion: Int
+) : SdkSandboxControllerCompat.SandboxControllerImpl {
+ override fun getSandboxedSdks(): List<SandboxedSdkCompat> {
+ return implFromClient.getSandboxedSdks()
+ }
+
+ override fun registerSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ): IBinder {
+ if (clientVersion < 3) {
+ throw UnsupportedOperationException(
+ "Client library version doesn't support SdkActivities"
+ )
+ }
+ return implFromClient.registerSdkSandboxActivityHandler(handlerCompat)
+ }
+
+ override fun unregisterSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ) {
+ if (clientVersion < 3) {
+ throw UnsupportedOperationException(
+ "Client library version doesn't support SdkActivities"
+ )
+ }
+ implFromClient.unregisterSdkSandboxActivityHandler(handlerCompat)
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/NoOpImpl.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/NoOpImpl.kt
index cc0bfa3..4bf7e4d 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/NoOpImpl.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/NoOpImpl.kt
@@ -16,7 +16,9 @@
package androidx.privacysandbox.sdkruntime.core.controller.impl
+import android.os.IBinder
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
/**
@@ -24,4 +26,17 @@
*/
internal class NoOpImpl : SdkSandboxControllerCompat.SandboxControllerImpl {
override fun getSandboxedSdks(): List<SandboxedSdkCompat> = emptyList()
+
+ override fun registerSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ):
+ IBinder {
+ throw UnsupportedOperationException("Not supported")
+ }
+
+ override fun unregisterSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ) {
+ throw UnsupportedOperationException("Not supported")
+ }
}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/PlatformImpl.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/PlatformImpl.kt
index ce2e035..d075eb7 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/PlatformImpl.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/PlatformImpl.kt
@@ -18,10 +18,12 @@
import android.app.sdksandbox.sdkprovider.SdkSandboxController
import android.content.Context
+import android.os.IBinder
import android.os.ext.SdkExtensions.AD_SERVICES
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresExtension
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
/**
@@ -29,7 +31,7 @@
*/
@RequiresApi(33)
@RequiresExtension(extension = AD_SERVICES, version = 5)
-internal class PlatformImpl(
+internal open class PlatformImpl(
private val controller: SdkSandboxController
) : SdkSandboxControllerCompat.SandboxControllerImpl {
@@ -39,6 +41,18 @@
.map { platformSdk -> SandboxedSdkCompat(platformSdk) }
}
+ override fun registerSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ): IBinder {
+ throw UnsupportedOperationException("This API only available for devices run on Android U+")
+ }
+
+ override fun unregisterSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ) {
+ throw UnsupportedOperationException("This API only available for devices run on Android U+")
+ }
+
companion object {
fun from(context: Context): PlatformImpl {
val sdkSandboxController = context.getSystemService(SdkSandboxController::class.java)
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/PlatformUDCImpl.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/PlatformUDCImpl.kt
new file mode 100644
index 0000000..a1e3aca
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/PlatformUDCImpl.kt
@@ -0,0 +1,137 @@
+/*
+ * 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.privacysandbox.sdkruntime.core.controller.impl
+
+import android.app.Activity
+import android.app.Application
+import android.app.sdksandbox.sdkprovider.SdkSandboxActivityHandler
+import android.app.sdksandbox.sdkprovider.SdkSandboxController
+import android.content.Context
+import android.os.Bundle
+import android.os.IBinder
+import android.os.ext.SdkExtensions
+import androidx.activity.OnBackPressedDispatcher
+import androidx.annotation.RequiresApi
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleRegistry
+
+/**
+ * Implementation that delegates to platform [SdkSandboxController] for Android U.
+ */
+@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
+@RequiresApi(34)
+internal class PlatformUDCImpl(
+ private val controller: SdkSandboxController
+) : PlatformImpl(controller) {
+
+ private val compatToPlatformMap =
+ hashMapOf<SdkSandboxActivityHandlerCompat, SdkSandboxActivityHandler>()
+
+ override fun registerSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ): IBinder {
+ synchronized(compatToPlatformMap) {
+ val platformHandler: SdkSandboxActivityHandler =
+ compatToPlatformMap[handlerCompat]
+ ?: SdkSandboxActivityHandler { platformActivity: Activity ->
+ handlerCompat.onActivityCreated(ActivityHolderImpl(platformActivity))
+ }
+ val token = controller.registerSdkSandboxActivityHandler(platformHandler)
+ compatToPlatformMap[handlerCompat] = platformHandler
+ return token
+ }
+ }
+
+ override fun unregisterSdkSandboxActivityHandler(
+ handlerCompat: SdkSandboxActivityHandlerCompat
+ ) {
+ synchronized(compatToPlatformMap) {
+ val platformHandler: SdkSandboxActivityHandler =
+ compatToPlatformMap[handlerCompat] ?: return
+ controller.unregisterSdkSandboxActivityHandler(platformHandler)
+ compatToPlatformMap.remove(handlerCompat)
+ }
+ }
+
+ internal class ActivityHolderImpl(
+ private val platformActivity: Activity
+ ) : ActivityHolder {
+ private val dispatcher = OnBackPressedDispatcher {}
+ private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
+
+ init {
+ // TODO(b/276315438) Set android:enableOnBackInvokedCallback="true" when
+ // creating the manifest file
+ dispatcher.setOnBackInvokedDispatcher(platformActivity.onBackInvokedDispatcher)
+ proxyLifeCycleEvents()
+ }
+
+ override fun getActivity(): Activity {
+ return platformActivity
+ }
+
+ override fun getOnBackPressedDispatcher(): OnBackPressedDispatcher {
+ return dispatcher
+ }
+
+ override val lifecycle: Lifecycle
+ get() = lifecycleRegistry
+
+ private fun proxyLifeCycleEvents() {
+ val callback = object : Application.ActivityLifecycleCallbacks {
+ override fun onActivityCreated(p0: Activity, p1: Bundle?) {
+ lifecycleRegistry.currentState = Lifecycle.State.CREATED
+ }
+
+ override fun onActivityStarted(p0: Activity) {
+ lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ }
+
+ override fun onActivityResumed(p0: Activity) {
+ lifecycleRegistry.currentState = Lifecycle.State.RESUMED
+ }
+
+ override fun onActivityPaused(p0: Activity) {
+ lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ }
+
+ override fun onActivityStopped(p0: Activity) {
+ lifecycleRegistry.currentState = Lifecycle.State.CREATED
+ }
+
+ override fun onActivityDestroyed(p0: Activity) {
+ lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+ }
+
+ override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {
+ // No need for proxying
+ }
+ }
+ platformActivity.registerActivityLifecycleCallbacks(callback)
+ }
+ }
+
+ companion object {
+ fun from(context: Context): PlatformImpl {
+ val sdkSandboxController = context.getSystemService(SdkSandboxController::class.java)
+ return PlatformUDCImpl(sdkSandboxController)
+ }
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-client/api/current.txt b/privacysandbox/ui/ui-client/api/current.txt
index c3fc560..ab2bf39 100644
--- a/privacysandbox/ui/ui-client/api/current.txt
+++ b/privacysandbox/ui/ui-client/api/current.txt
@@ -1,11 +1,20 @@
// Signature format: 4.0
package androidx.privacysandbox.ui.client {
+ public interface LocalSdkActivityLauncher<T extends android.app.Activity & androidx.lifecycle.LifecycleOwner> extends androidx.privacysandbox.ui.core.SdkActivityLauncher {
+ method public void dispose();
+ }
+
@RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class SandboxedUiAdapterFactory {
method public androidx.privacysandbox.ui.core.SandboxedUiAdapter createFromCoreLibInfo(android.os.Bundle coreLibInfo);
field public static final androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory INSTANCE;
}
+ public final class SdkActivityLaunchers {
+ method public static <T extends android.app.Activity & androidx.lifecycle.LifecycleOwner> androidx.privacysandbox.ui.client.LocalSdkActivityLauncher<T> createSdkActivityLauncher(T, kotlin.jvm.functions.Function0<java.lang.Boolean> allowLaunch);
+ method public static android.os.Bundle toLauncherInfo(androidx.privacysandbox.ui.core.SdkActivityLauncher);
+ }
+
}
package androidx.privacysandbox.ui.client.view {
diff --git a/privacysandbox/ui/ui-client/api/public_plus_experimental_current.txt b/privacysandbox/ui/ui-client/api/public_plus_experimental_current.txt
index c3fc560..ab2bf39 100644
--- a/privacysandbox/ui/ui-client/api/public_plus_experimental_current.txt
+++ b/privacysandbox/ui/ui-client/api/public_plus_experimental_current.txt
@@ -1,11 +1,20 @@
// Signature format: 4.0
package androidx.privacysandbox.ui.client {
+ public interface LocalSdkActivityLauncher<T extends android.app.Activity & androidx.lifecycle.LifecycleOwner> extends androidx.privacysandbox.ui.core.SdkActivityLauncher {
+ method public void dispose();
+ }
+
@RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class SandboxedUiAdapterFactory {
method public androidx.privacysandbox.ui.core.SandboxedUiAdapter createFromCoreLibInfo(android.os.Bundle coreLibInfo);
field public static final androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory INSTANCE;
}
+ public final class SdkActivityLaunchers {
+ method public static <T extends android.app.Activity & androidx.lifecycle.LifecycleOwner> androidx.privacysandbox.ui.client.LocalSdkActivityLauncher<T> createSdkActivityLauncher(T, kotlin.jvm.functions.Function0<java.lang.Boolean> allowLaunch);
+ method public static android.os.Bundle toLauncherInfo(androidx.privacysandbox.ui.core.SdkActivityLauncher);
+ }
+
}
package androidx.privacysandbox.ui.client.view {
diff --git a/privacysandbox/ui/ui-client/api/restricted_current.txt b/privacysandbox/ui/ui-client/api/restricted_current.txt
index c3fc560..ab2bf39 100644
--- a/privacysandbox/ui/ui-client/api/restricted_current.txt
+++ b/privacysandbox/ui/ui-client/api/restricted_current.txt
@@ -1,11 +1,20 @@
// Signature format: 4.0
package androidx.privacysandbox.ui.client {
+ public interface LocalSdkActivityLauncher<T extends android.app.Activity & androidx.lifecycle.LifecycleOwner> extends androidx.privacysandbox.ui.core.SdkActivityLauncher {
+ method public void dispose();
+ }
+
@RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class SandboxedUiAdapterFactory {
method public androidx.privacysandbox.ui.core.SandboxedUiAdapter createFromCoreLibInfo(android.os.Bundle coreLibInfo);
field public static final androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory INSTANCE;
}
+ public final class SdkActivityLaunchers {
+ method public static <T extends android.app.Activity & androidx.lifecycle.LifecycleOwner> androidx.privacysandbox.ui.client.LocalSdkActivityLauncher<T> createSdkActivityLauncher(T, kotlin.jvm.functions.Function0<java.lang.Boolean> allowLaunch);
+ method public static android.os.Bundle toLauncherInfo(androidx.privacysandbox.ui.core.SdkActivityLauncher);
+ }
+
}
package androidx.privacysandbox.ui.client.view {
diff --git a/privacysandbox/ui/ui-client/build.gradle b/privacysandbox/ui/ui-client/build.gradle
index 747c1d1..9a4ac4c 100644
--- a/privacysandbox/ui/ui-client/build.gradle
+++ b/privacysandbox/ui/ui-client/build.gradle
@@ -25,7 +25,14 @@
dependencies {
api(libs.kotlinStdlib)
api("androidx.annotation:annotation:1.1.0")
+
+ // For BundleCompat#putBinder.
+ // TODO(b/280561849): Use stable version when available.
+ implementation("androidx.core:core:1.12.0-alpha03")
+ implementation("androidx.lifecycle:lifecycle-common:2.2.0")
+ implementation project(path: ':privacysandbox:sdkruntime:sdkruntime-client')
implementation project(path: ':privacysandbox:ui:ui-core')
+
androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
@@ -34,11 +41,18 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
+ androidTestImplementation(libs.espressoIntents)
+ androidTestImplementation(libs.espressoCore)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.multidex)
androidTestImplementation project(path: ':appcompat:appcompat')
}
android {
namespace "androidx.privacysandbox.ui.client"
+ defaultConfig {
+ multiDexEnabled true
+ }
}
androidx {
diff --git a/privacysandbox/ui/ui-client/src/androidTest/AndroidManifest.xml b/privacysandbox/ui/ui-client/src/androidTest/AndroidManifest.xml
index e114f95..b2720e1 100644
--- a/privacysandbox/ui/ui-client/src/androidTest/AndroidManifest.xml
+++ b/privacysandbox/ui/ui-client/src/androidTest/AndroidManifest.xml
@@ -16,6 +16,7 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:supportsRtl="true"
+ android:name="androidx.multidex.MultiDexApplication"
android:theme="@style/Theme.AppCompat">
<activity
android:name="androidx.privacysandbox.ui.client.test.UiLibActivity"
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/CreateSdkActivityLauncherTest.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/CreateSdkActivityLauncherTest.kt
new file mode 100644
index 0000000..3b50998
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/CreateSdkActivityLauncherTest.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.privacysandbox.ui.client.test
+
+import android.app.Activity
+import android.app.Instrumentation.ActivityResult
+import android.content.Intent
+import android.os.Binder
+import android.os.Build
+import androidx.lifecycle.Lifecycle
+import androidx.privacysandbox.ui.client.createSdkActivityLauncher
+import androidx.test.espresso.intent.Intents.intended
+import androidx.test.espresso.intent.Intents.intending
+import androidx.test.espresso.intent.Intents.times
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
+import androidx.test.espresso.intent.rule.IntentsRule
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.hamcrest.Matchers.`is`
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@SmallTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+class CreateSdkActivityLauncherTest {
+ @get:Rule
+ var activityScenarioRule = ActivityScenarioRule(UiLibActivity::class.java)
+
+ @get:Rule
+ var intentsRule = IntentsRule()
+
+ private val sdkSandboxActivityMatcher =
+ hasAction(`is`("android.app.sdksandbox.action.START_SANDBOXED_ACTIVITY"))
+
+ @Before
+ fun setUp() {
+ // Intercepts intent to start sandboxed activity and immediately return a result.
+ // This allows us to avoid loading and setting up an SDK just for checking if activities are
+ // launched.
+ intending(sdkSandboxActivityMatcher)
+ .respondWith(ActivityResult(Activity.RESULT_OK, Intent()))
+ }
+
+ @Test
+ fun returnedLauncher_launchesActivitiesWhenAllowed() = runBlocking {
+ val launcher = activityScenarioRule.withActivity { this.createSdkActivityLauncher { true } }
+
+ val result = launcher.launchSdkActivity(Binder())
+
+ assertThat(result).isTrue()
+ intended(sdkSandboxActivityMatcher, times(1))
+ }
+
+ @Test
+ fun returnedLauncher_rejectsActivityLaunchesAccordingToPredicate() = runBlocking {
+ val launcher =
+ activityScenarioRule.withActivity { this.createSdkActivityLauncher { false } }
+
+ val result = launcher.launchSdkActivity(Binder())
+
+ assertThat(result).isFalse()
+ intended(sdkSandboxActivityMatcher, times(0))
+ }
+
+ @Test
+ fun returnedLauncher_rejectsActivityLaunchesWhenDisposed() = runBlocking {
+ val launcher = activityScenarioRule.withActivity { this.createSdkActivityLauncher { true } }
+ launcher.dispose()
+
+ val result = launcher.launchSdkActivity(Binder())
+
+ assertThat(result).isFalse()
+ intended(sdkSandboxActivityMatcher, times(0))
+ }
+
+ @Test
+ fun returnedLauncher_disposeCanBeCalledMultipleTimes() = runBlocking {
+ val launcher = activityScenarioRule.withActivity { this.createSdkActivityLauncher { true } }
+ launcher.dispose()
+
+ val result = launcher.launchSdkActivity(Binder())
+ launcher.dispose()
+ launcher.dispose()
+
+ assertThat(result).isFalse()
+ intended(sdkSandboxActivityMatcher, times(0))
+ }
+
+ @Test
+ fun returnedLauncher_rejectsActivityLaunchesWhenHostActivityIsDestroyed() = runBlocking {
+ val launcher = activityScenarioRule.withActivity { this.createSdkActivityLauncher { true } }
+ activityScenarioRule.scenario.moveToState(Lifecycle.State.DESTROYED)
+
+ val result = launcher.launchSdkActivity(Binder())
+
+ assertThat(result).isFalse()
+ intended(sdkSandboxActivityMatcher, times(0))
+ }
+
+ @Test
+ fun returnedLauncher_rejectsActivityLaunchesWhenHostActivityWasAlreadyDestroyed() =
+ runBlocking {
+ val activity = activityScenarioRule.withActivity { this }
+ activityScenarioRule.scenario.moveToState(Lifecycle.State.DESTROYED)
+ val launcher = activity.createSdkActivityLauncher { true }
+
+ val result = launcher.launchSdkActivity(Binder())
+
+ assertThat(result).isFalse()
+ intended(sdkSandboxActivityMatcher, times(0))
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SdkActivityLaunchers.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SdkActivityLaunchers.kt
new file mode 100644
index 0000000..2d4e082
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SdkActivityLaunchers.kt
@@ -0,0 +1,163 @@
+/*
+ * 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("SdkActivityLaunchers")
+
+package androidx.privacysandbox.ui.client
+
+import android.app.Activity
+import android.os.Bundle
+import android.os.IBinder
+import androidx.core.os.BundleCompat
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat
+import androidx.privacysandbox.ui.core.ISdkActivityLauncher
+import androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback
+import androidx.privacysandbox.ui.core.ProtocolConstants.sdkActivityLauncherBinderKey
+import androidx.privacysandbox.ui.core.SdkActivityLauncher
+import java.util.concurrent.atomic.AtomicReference
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Returns an SdkActivityLauncher that launches activities on behalf of an SDK by using this
+ * activity as a starting context.
+ *
+ * @param T the current activity from which new SDK activities will be launched. If this activity is
+ * destroyed any further SDK activity launches will simply be ignored.
+ * @param allowLaunch predicate called each time an activity is about to be launched by the
+ * SDK, the activity will only be launched if it returns true.
+ */
+fun <T> T.createSdkActivityLauncher(
+ allowLaunch: () -> Boolean
+): LocalSdkActivityLauncher<T>
+ where T : Activity, T : LifecycleOwner {
+ val cancellationJob = Job(parent = lifecycleScope.coroutineContext[Job])
+ val launcher = LocalSdkActivityLauncherImpl(
+ activity = this,
+ allowLaunch = allowLaunch,
+ onDispose = { cancellationJob.cancel() },
+ )
+ cancellationJob.invokeOnCompletion {
+ launcher.dispose()
+ }
+ return launcher
+}
+
+/**
+ * Returns a [Bundle] with the information necessary to recreate this launcher.
+ * Possibly in a different process.
+ */
+fun SdkActivityLauncher.toLauncherInfo(): Bundle {
+ val binderDelegate = SdkActivityLauncherBinderDelegate(this)
+ return Bundle().also { bundle ->
+ BundleCompat.putBinder(bundle, sdkActivityLauncherBinderKey, binderDelegate)
+ }
+}
+
+/**
+ * Local implementation of an SDK Activity launcher.
+ *
+ * It allows callers in the app process to dispose resources used to launch SDK activities.
+ */
+interface LocalSdkActivityLauncher<T> : SdkActivityLauncher where T : Activity, T : LifecycleOwner {
+ /**
+ * Clears references used to launch activities.
+ *
+ * After this method is called all further attempts to launch activities wil be rejected.
+ * Doesn't do anything if the launcher was already disposed of.
+ */
+ fun dispose()
+}
+
+private class LocalSdkActivityLauncherImpl<T>(
+ activity: T,
+ allowLaunch: () -> Boolean,
+ onDispose: () -> Unit
+) : LocalSdkActivityLauncher<T> where T : Activity, T : LifecycleOwner {
+
+ /** Internal state for [LocalSdkActivityLauncher], cleared when the launcher is disposed. */
+ private class LocalLauncherState<T>(
+ val activity: T,
+ val allowLaunch: () -> Boolean,
+ val sdkSandboxManager: SdkSandboxManagerCompat,
+ val onDispose: () -> Unit
+ ) where T : Activity, T : LifecycleOwner
+
+ private val stateReference: AtomicReference<LocalLauncherState<T>?> =
+ AtomicReference<LocalLauncherState<T>?>(
+ LocalLauncherState(
+ activity,
+ allowLaunch,
+ SdkSandboxManagerCompat.from(activity),
+ onDispose
+ )
+ )
+
+ override suspend fun launchSdkActivity(
+ sdkActivityHandlerToken: IBinder
+ ): Boolean {
+ val state = stateReference.get() ?: return false
+ return withContext(Dispatchers.Main.immediate) {
+ state.run {
+ allowLaunch().also { didAllowLaunch ->
+ if (didAllowLaunch) {
+ sdkSandboxManager.startSdkSandboxActivity(activity, sdkActivityHandlerToken)
+ }
+ }
+ }
+ }
+ }
+
+ override fun dispose() {
+ stateReference.getAndSet(null)?.run {
+ onDispose()
+ }
+ }
+}
+
+private class SdkActivityLauncherBinderDelegate(private val launcher: SdkActivityLauncher) :
+ ISdkActivityLauncher.Stub() {
+
+ private val coroutineScope = CoroutineScope(Dispatchers.Main)
+
+ override fun launchSdkActivity(
+ sdkActivityHandlerToken: IBinder?,
+ callback: ISdkActivityLauncherCallback?
+ ) {
+ requireNotNull(sdkActivityHandlerToken)
+ requireNotNull(callback)
+
+ coroutineScope.launch {
+ val accepted = try {
+ launcher.launchSdkActivity(sdkActivityHandlerToken)
+ } catch (t: Throwable) {
+ callback.onLaunchError(t.message ?: "Unknown error launching SDK activity.")
+ return@launch
+ }
+
+ if (accepted) {
+ callback.onLaunchAccepted(sdkActivityHandlerToken)
+ } else {
+ callback.onLaunchRejected(sdkActivityHandlerToken)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-core/api/current.txt b/privacysandbox/ui/ui-core/api/current.txt
index 4154b4b..db32e1e 100644
--- a/privacysandbox/ui/ui-core/api/current.txt
+++ b/privacysandbox/ui/ui-core/api/current.txt
@@ -20,6 +20,10 @@
method public void onSessionOpened(androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session session);
}
+ public interface SdkActivityLauncher {
+ method public suspend Object? launchSdkActivity(android.os.IBinder sdkActivityHandlerToken, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ }
+
public final class SdkRuntimeUiLibVersions {
method public int getClientVersion();
property public final int clientVersion;
diff --git a/privacysandbox/ui/ui-core/api/public_plus_experimental_current.txt b/privacysandbox/ui/ui-core/api/public_plus_experimental_current.txt
index 4154b4b..db32e1e 100644
--- a/privacysandbox/ui/ui-core/api/public_plus_experimental_current.txt
+++ b/privacysandbox/ui/ui-core/api/public_plus_experimental_current.txt
@@ -20,6 +20,10 @@
method public void onSessionOpened(androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session session);
}
+ public interface SdkActivityLauncher {
+ method public suspend Object? launchSdkActivity(android.os.IBinder sdkActivityHandlerToken, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ }
+
public final class SdkRuntimeUiLibVersions {
method public int getClientVersion();
property public final int clientVersion;
diff --git a/privacysandbox/ui/ui-core/api/restricted_current.txt b/privacysandbox/ui/ui-core/api/restricted_current.txt
index 4154b4b..db32e1e 100644
--- a/privacysandbox/ui/ui-core/api/restricted_current.txt
+++ b/privacysandbox/ui/ui-core/api/restricted_current.txt
@@ -20,6 +20,10 @@
method public void onSessionOpened(androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session session);
}
+ public interface SdkActivityLauncher {
+ method public suspend Object? launchSdkActivity(android.os.IBinder sdkActivityHandlerToken, kotlin.coroutines.Continuation<? super java.lang.Boolean>);
+ }
+
public final class SdkRuntimeUiLibVersions {
method public int getClientVersion();
property public final int clientVersion;
diff --git a/privacysandbox/ui/ui-core/lint-baseline.xml b/privacysandbox/ui/ui-core/lint-baseline.xml
index c6aa1c9..23022fb 100644
--- a/privacysandbox/ui/ui-core/lint-baseline.xml
+++ b/privacysandbox/ui/ui-core/lint-baseline.xml
@@ -37,4 +37,22 @@
file="src/main/aidl/androidx/privacysandbox/ui/core/ISandboxedUiAdapter.aidl"/>
</issue>
+ <issue
+ id="RequireUnstableAidlAnnotation"
+ message="Unstable AIDL files must be annotated with `@RequiresOptIn` marker"
+ errorLine1="oneway interface ISdkActivityLauncher {"
+ errorLine2="^">
+ <location
+ file="src/main/aidl/androidx/privacysandbox/ui/core/ISdkActivityLauncher.aidl"/>
+ </issue>
+
+ <issue
+ id="RequireUnstableAidlAnnotation"
+ message="Unstable AIDL files must be annotated with `@RequiresOptIn` marker"
+ errorLine1="oneway interface ISdkActivityLauncherCallback {"
+ errorLine2="^">
+ <location
+ file="src/main/aidl/androidx/privacysandbox/ui/core/ISdkActivityLauncherCallback.aidl"/>
+ </issue>
+
</issues>
diff --git a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/ISdkActivityLauncher.aidl b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/ISdkActivityLauncher.aidl
new file mode 100644
index 0000000..76cfa3c
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/ISdkActivityLauncher.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 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.privacysandbox.ui.core;
+
+import androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback;
+
+/* @hide */
+oneway interface ISdkActivityLauncher {
+ void launchSdkActivity(
+ in IBinder sdkActivityHandlerToken,
+ ISdkActivityLauncherCallback callback) = 1;
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/ISdkActivityLauncherCallback.aidl b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/ISdkActivityLauncherCallback.aidl
new file mode 100644
index 0000000..22b7beb
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/ISdkActivityLauncherCallback.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 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.privacysandbox.ui.core;
+
+/* @hide */
+oneway interface ISdkActivityLauncherCallback {
+ void onLaunchAccepted(in IBinder sdkActivityHandlerToken) = 1;
+ void onLaunchRejected(in IBinder sdkActivityHandlerToken) = 2;
+ void onLaunchError(String message) = 3;
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/ProtocolConstants.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/ProtocolConstants.kt
new file mode 100644
index 0000000..5bff1c9
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/ProtocolConstants.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.privacysandbox.ui.core
+
+import androidx.annotation.RestrictTo
+
+/**
+ * Constants shared between UI library artifacts to establish an IPC protocol across library
+ * versions. Adding new constants is allowed, but **never change the value of a constant**, or
+ * you'll break binary compatibility between UI library versions.
+ *
+ * @suppress
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+object ProtocolConstants {
+ const val sdkActivityLauncherBinderKey = "sdkActivityLauncherBinderKey"
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SdkActivityLauncher.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SdkActivityLauncher.kt
new file mode 100644
index 0000000..edd3008
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SdkActivityLauncher.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.privacysandbox.ui.core
+
+import android.os.IBinder
+
+/**
+ * Interface that allows SDKs running in the Privacy Sandbox to launch activities.
+ *
+ * Apps can create launchers by calling
+ * [createActivityLauncher][androidx.privacysandbox.ui.client.createSdkActivityLauncher]
+ * from one of their activities.
+ *
+ * To send an [SdkActivityLauncher] to another process, they can call
+ * [toLauncherInfo][androidx.privacysandbox.ui.client.toLauncherInfo]
+ * and send the resulting bundle.
+ *
+ * SDKs can create launchers from an app-provided bundle by calling
+ * [createFromLauncherInfo][androidx.privacysandbox.ui.provider.SdkActivityLauncherFactory.createFromLauncherInfo].
+ */
+interface SdkActivityLauncher {
+
+ /**
+ * Tries to launch a new SDK activity using the given [sdkActivityHandlerToken],
+ * assumed to be registered in the [SdkSandboxControllerCompat][androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat].
+ *
+ * Returns true if the SDK activity intent was sent, false if the launch was rejected for any
+ * reason.
+ */
+ suspend fun launchSdkActivity(sdkActivityHandlerToken: IBinder): Boolean
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-provider/api/current.txt b/privacysandbox/ui/ui-provider/api/current.txt
index 20170b4..03fbefd 100644
--- a/privacysandbox/ui/ui-provider/api/current.txt
+++ b/privacysandbox/ui/ui-provider/api/current.txt
@@ -5,5 +5,10 @@
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static android.os.Bundle toCoreLibInfo(androidx.privacysandbox.ui.core.SandboxedUiAdapter, android.content.Context context);
}
+ public final class SdkActivityLauncherFactory {
+ method public static androidx.privacysandbox.ui.core.SdkActivityLauncher fromLauncherInfo(android.os.Bundle launcherInfo);
+ field public static final androidx.privacysandbox.ui.provider.SdkActivityLauncherFactory INSTANCE;
+ }
+
}
diff --git a/privacysandbox/ui/ui-provider/api/public_plus_experimental_current.txt b/privacysandbox/ui/ui-provider/api/public_plus_experimental_current.txt
index 20170b4..03fbefd 100644
--- a/privacysandbox/ui/ui-provider/api/public_plus_experimental_current.txt
+++ b/privacysandbox/ui/ui-provider/api/public_plus_experimental_current.txt
@@ -5,5 +5,10 @@
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static android.os.Bundle toCoreLibInfo(androidx.privacysandbox.ui.core.SandboxedUiAdapter, android.content.Context context);
}
+ public final class SdkActivityLauncherFactory {
+ method public static androidx.privacysandbox.ui.core.SdkActivityLauncher fromLauncherInfo(android.os.Bundle launcherInfo);
+ field public static final androidx.privacysandbox.ui.provider.SdkActivityLauncherFactory INSTANCE;
+ }
+
}
diff --git a/privacysandbox/ui/ui-provider/api/restricted_current.txt b/privacysandbox/ui/ui-provider/api/restricted_current.txt
index 20170b4..03fbefd 100644
--- a/privacysandbox/ui/ui-provider/api/restricted_current.txt
+++ b/privacysandbox/ui/ui-provider/api/restricted_current.txt
@@ -5,5 +5,10 @@
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static android.os.Bundle toCoreLibInfo(androidx.privacysandbox.ui.core.SandboxedUiAdapter, android.content.Context context);
}
+ public final class SdkActivityLauncherFactory {
+ method public static androidx.privacysandbox.ui.core.SdkActivityLauncher fromLauncherInfo(android.os.Bundle launcherInfo);
+ field public static final androidx.privacysandbox.ui.provider.SdkActivityLauncherFactory INSTANCE;
+ }
+
}
diff --git a/privacysandbox/ui/ui-provider/build.gradle b/privacysandbox/ui/ui-provider/build.gradle
index a89f128..c86e7f8 100644
--- a/privacysandbox/ui/ui-provider/build.gradle
+++ b/privacysandbox/ui/ui-provider/build.gradle
@@ -25,7 +25,13 @@
dependencies {
api(libs.kotlinStdlib)
api("androidx.annotation:annotation:1.1.0")
+
implementation project(path: ':privacysandbox:ui:ui-core')
+ // For BundleCompat#getBinder.
+ // TODO(b/280561849): Use stable version when available.
+ implementation("androidx.core:core:1.12.0-alpha03")
+ implementation(libs.kotlinCoroutinesCore)
+
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.testExtJunit)
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/SdkActivityLauncherFactory.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/SdkActivityLauncherFactory.kt
new file mode 100644
index 0000000..9d73f49
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/SdkActivityLauncherFactory.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.privacysandbox.ui.provider
+
+import android.os.Bundle
+import android.os.IBinder
+import androidx.core.os.BundleCompat
+import androidx.privacysandbox.ui.core.ISdkActivityLauncher
+import androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback
+import androidx.privacysandbox.ui.core.ProtocolConstants.sdkActivityLauncherBinderKey
+import androidx.privacysandbox.ui.core.SdkActivityLauncher
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+object SdkActivityLauncherFactory {
+
+ /**
+ * Creates a [SdkActivityLauncher] using the given [launcherInfo] Bundle.
+ *
+ * You can create such a Bundle by calling [toLauncherInfo][androidx.privacysandbox.ui.client.toLauncherInfo].
+ * A [launcherInfo] is expected to have a valid SdkActivityLauncher Binder with
+ * `"sdkActivityLauncherBinderKey"` for a key, [IllegalArgumentException] is thrown otherwise.
+ */
+ @JvmStatic
+ fun fromLauncherInfo(launcherInfo: Bundle): SdkActivityLauncher {
+ val remote: ISdkActivityLauncher? = ISdkActivityLauncher.Stub.asInterface(
+ BundleCompat.getBinder(launcherInfo, sdkActivityLauncherBinderKey)
+ )
+ requireNotNull(remote) { "Invalid SdkActivityLauncher info bundle." }
+ return SdkActivityLauncherProxy(remote)
+ }
+
+ private class SdkActivityLauncherProxy(
+ private val remote: ISdkActivityLauncher
+ ) : SdkActivityLauncher {
+ override suspend fun launchSdkActivity(sdkActivityHandlerToken: IBinder): Boolean =
+ suspendCancellableCoroutine {
+ remote.launchSdkActivity(
+ sdkActivityHandlerToken,
+ object : ISdkActivityLauncherCallback.Stub() {
+ override fun onLaunchAccepted(sdkActivityHandlerToken: IBinder?) {
+ it.resume(true)
+ }
+
+ override fun onLaunchRejected(sdkActivityHandlerToken: IBinder?) {
+ it.resume(false)
+ }
+
+ override fun onLaunchError(message: String?) {
+ it.resumeWithException(RuntimeException(message))
+ }
+ })
+ }
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-tests/build.gradle b/privacysandbox/ui/ui-tests/build.gradle
index a675a27..cf66642 100644
--- a/privacysandbox/ui/ui-tests/build.gradle
+++ b/privacysandbox/ui/ui-tests/build.gradle
@@ -26,12 +26,14 @@
implementation project(path: ':privacysandbox:ui:ui-core')
implementation project(path: ':privacysandbox:ui:ui-client')
implementation project(path: ':privacysandbox:ui:ui-provider')
+ implementation(libs.kotlinStdlib)
+
androidTestImplementation(project(":internal-testutils-runtime"))
- api(libs.kotlinStdlib)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.kotlinCoroutinesCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
diff --git a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/activity/SdkActivityLauncherBundlingTest.kt b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/activity/SdkActivityLauncherBundlingTest.kt
new file mode 100644
index 0000000..6971f7c
--- /dev/null
+++ b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/activity/SdkActivityLauncherBundlingTest.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.privacysandbox.ui.tests.activity
+
+import android.os.Binder
+import android.os.IBinder
+import androidx.privacysandbox.ui.client.toLauncherInfo
+import androidx.privacysandbox.ui.core.SdkActivityLauncher
+import androidx.privacysandbox.ui.provider.SdkActivityLauncherFactory
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SdkActivityLauncherBundlingTest {
+
+ @Test
+ fun unbundledSdkActivityLauncher_launchesActivities(): Unit = runBlocking {
+ val launcher = TestSdkActivityLauncher()
+ val launcherInfo = launcher.toLauncherInfo()
+
+ val unbundledLauncher = SdkActivityLauncherFactory.fromLauncherInfo(launcherInfo)
+ val token = Binder()
+ val result = unbundledLauncher.launchSdkActivity(token)
+
+ assertThat(result).isTrue()
+ assertThat(launcher.tokensReceived).containsExactly(token)
+ }
+
+ @Test
+ fun unbundledSdkActivityLauncher_rejectsActivityLaunches(): Unit = runBlocking {
+ val launcher = TestSdkActivityLauncher()
+ launcher.allowActivityLaunches = false
+ val launcherInfo = launcher.toLauncherInfo()
+
+ val unbundledLauncher = SdkActivityLauncherFactory.fromLauncherInfo(launcherInfo)
+ val token = Binder()
+ val result = unbundledLauncher.launchSdkActivity(token)
+
+ assertThat(result).isFalse()
+ assertThat(launcher.tokensReceived).containsExactly(token)
+ }
+
+ class TestSdkActivityLauncher : SdkActivityLauncher {
+ var allowActivityLaunches: Boolean = true
+
+ var tokensReceived = mutableListOf<IBinder>()
+
+ override suspend fun launchSdkActivity(sdkActivityHandlerToken: IBinder):
+ Boolean {
+ tokensReceived.add(sdkActivityHandlerToken)
+ return allowActivityLaunches
+ }
+ }
+}
\ No newline at end of file
diff --git a/profileinstaller/profileinstaller/build.gradle b/profileinstaller/profileinstaller/build.gradle
index ba3d60d..793c5e5 100644
--- a/profileinstaller/profileinstaller/build.gradle
+++ b/profileinstaller/profileinstaller/build.gradle
@@ -32,7 +32,7 @@
}
androidx {
- name = "androidx.profileinstaller:profileinstaller"
+ name = "Profile Installer"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2021"
description = "Allows libraries to prepopulate ahead of time compilation traces to be read by" +
diff --git a/recommendation/recommendation/build.gradle b/recommendation/recommendation/build.gradle
index 25a96bf..50c69b0 100644
--- a/recommendation/recommendation/build.gradle
+++ b/recommendation/recommendation/build.gradle
@@ -17,7 +17,7 @@
}
androidx {
- name = "Android Support Recommendation"
+ name = "Recommendation"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2015"
description = "Android Support Recommendation"
diff --git a/recyclerview/recyclerview-selection/build.gradle b/recyclerview/recyclerview-selection/build.gradle
index 49f9cfd..12c9e66 100644
--- a/recyclerview/recyclerview-selection/build.gradle
+++ b/recyclerview/recyclerview-selection/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "Android RecyclerView Selection"
+ name = "RecyclerView Selection"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.RECYCLERVIEW_SELECTION
inceptionYear = "2017"
diff --git a/recyclerview/recyclerview/api/api_lint.ignore b/recyclerview/recyclerview/api/api_lint.ignore
index ee7fa16..463599f 100644
--- a/recyclerview/recyclerview/api/api_lint.ignore
+++ b/recyclerview/recyclerview/api/api_lint.ignore
@@ -161,12 +161,6 @@
Internal field mLayoutManager must not be exposed
-InvalidNullabilityOverride: androidx.recyclerview.widget.RecyclerView#draw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `c` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.recyclerview.widget.RecyclerView#drawChild(android.graphics.Canvas, android.view.View, long) parameter #0:
- Invalid nullability on parameter `canvas` in method `drawChild`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.recyclerview.widget.RecyclerView#onDraw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `c` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
InvalidNullabilityOverride: androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate.ItemDelegate#dispatchPopulateAccessibilityEvent(android.view.View, android.view.accessibility.AccessibilityEvent) parameter #0:
Invalid nullability on parameter `host` in method `dispatchPopulateAccessibilityEvent`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
InvalidNullabilityOverride: androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate.ItemDelegate#dispatchPopulateAccessibilityEvent(android.view.View, android.view.accessibility.AccessibilityEvent) parameter #1:
@@ -551,6 +545,10 @@
Missing nullability on parameter `container` in method `dispatchRestoreInstanceState`
MissingNullability: androidx.recyclerview.widget.RecyclerView#dispatchSaveInstanceState(android.util.SparseArray<android.os.Parcelable>) parameter #0:
Missing nullability on parameter `container` in method `dispatchSaveInstanceState`
+MissingNullability: androidx.recyclerview.widget.RecyclerView#draw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `c` in method `draw`
+MissingNullability: androidx.recyclerview.widget.RecyclerView#drawChild(android.graphics.Canvas, android.view.View, long) parameter #0:
+ Missing nullability on parameter `canvas` in method `drawChild`
MissingNullability: androidx.recyclerview.widget.RecyclerView#drawChild(android.graphics.Canvas, android.view.View, long) parameter #1:
Missing nullability on parameter `child` in method `drawChild`
MissingNullability: androidx.recyclerview.widget.RecyclerView#findViewHolderForItemId(long):
@@ -573,6 +571,8 @@
Missing nullability on method `getAccessibilityClassName` return
MissingNullability: androidx.recyclerview.widget.RecyclerView#getChildViewHolder(android.view.View):
Missing nullability on method `getChildViewHolder` return
+MissingNullability: androidx.recyclerview.widget.RecyclerView#onDraw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `c` in method `onDraw`
MissingNullability: androidx.recyclerview.widget.RecyclerView#onGenericMotionEvent(android.view.MotionEvent) parameter #0:
Missing nullability on parameter `event` in method `onGenericMotionEvent`
MissingNullability: androidx.recyclerview.widget.RecyclerView#onInterceptTouchEvent(android.view.MotionEvent) parameter #0:
diff --git a/recyclerview/recyclerview/build.gradle b/recyclerview/recyclerview/build.gradle
index aecfc856..2f52af1 100644
--- a/recyclerview/recyclerview/build.gradle
+++ b/recyclerview/recyclerview/build.gradle
@@ -8,7 +8,7 @@
dependencies {
api("androidx.annotation:annotation:1.1.0")
- api "androidx.core:core:1.7.0"
+ api(project(":core:core"))
implementation("androidx.collection:collection:1.0.0")
api("androidx.customview:customview:1.0.0")
implementation("androidx.customview:customview-poolingcontainer:1.0.0")
@@ -58,7 +58,7 @@
}
androidx {
- name = "Android Support RecyclerView"
+ name = "RecyclerView"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.RECYCLERVIEW
inceptionYear = "2014"
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
index a1b18f0..e0139e7 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
@@ -19,9 +19,14 @@
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_ACCESSIBILITY_FOCUS;
+import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_IN_DIRECTION;
+import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION;
import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL;
import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -30,6 +35,8 @@
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.app.UiAutomation;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.StateListDrawable;
@@ -39,11 +46,13 @@
import android.util.StateSet;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.GridView;
import android.widget.TextView;
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.test.annotation.UiThreadTest;
@@ -51,16 +60,17 @@
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
-import com.google.common.truth.Truth;
-
import org.hamcrest.CoreMatchers;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
@LargeTest
@@ -68,6 +78,9 @@
public class GridLayoutManagerTest extends BaseGridLayoutManagerTest {
private static final int[] SPAN_SIZES = new int[]{1, 1, 1, 2, 2, 2, 2, 3, 3, 2, 2, 2};
+
+ private static final int DEFAULT_ACCESSIBILITY_EVENT_TIMEOUT_MILLIS = 5000;
+
private final GridLayoutManager.SpanSizeLookup mSpanSizeLookupForSpanIndexTest =
new GridLayoutManager.SpanSizeLookup() {
@Override
@@ -964,8 +977,7 @@
waitForFirstLayout(recyclerView);
final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
- assertFalse(nodeInfo.getActionList().contains(
- AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+ assertFalse(nodeInfo.getActionList().contains(ACTION_SCROLL_TO_POSITION));
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
@@ -973,8 +985,7 @@
}
});
- assertFalse(nodeInfo.getActionList().contains(
- AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+ assertFalse(nodeInfo.getActionList().contains(ACTION_SCROLL_TO_POSITION));
}
@Test
@@ -985,8 +996,7 @@
waitForFirstLayout(recyclerView);
final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
- assertFalse(nodeInfo.getActionList().contains(
- AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+ assertFalse(nodeInfo.getActionList().contains(ACTION_SCROLL_TO_POSITION));
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
@@ -994,8 +1004,7 @@
}
});
- assertTrue(nodeInfo.getActionList().contains(
- AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+ assertTrue(nodeInfo.getActionList().contains(ACTION_SCROLL_TO_POSITION));
}
@Test
@@ -1102,6 +1111,508 @@
assertEquals(((TextView) mGlm.getChildAt(0)).getText(), "Item (6)");
}
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_withoutSpecifyingDirection()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android versions.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(6, HORIZONTAL);
+ setAccessibilityFocus(uiAutomation, mGlm.getChildAt(0));
+ final boolean[] returnValue = {false};
+ mActivityRule.runOnUiThread(
+ () -> {
+ returnValue[0] = mRecyclerView.getLayoutManager().performAccessibilityAction(
+ ACTION_SCROLL_IN_DIRECTION.getId(), null);
+ });
+ assertThat(returnValue[0]).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_withInvalidDirection()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android versions.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(6, HORIZONTAL);
+ setAccessibilityFocus(uiAutomation, mGlm.getChildAt(0));
+ runScrollInDirectionAndFail(-1);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_withoutSettingAccessibilityFocus()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android version.
+
+ // Return value of this call is not used.
+ setUpGridLayoutManagerAccessibilityTest(6, HORIZONTAL);
+ runScrollInDirectionAndFail(View.FOCUS_RIGHT);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusRight_vertical_withAvailableTarget()
+ throws Throwable {
+
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android version.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, VERTICAL);
+ /*
+ This generates the following grid:
+ 1 2 3
+ 4
+ */
+ runScrollInDirectionOnMultipleItemsAndSucceed(uiAutomation, View.FOCUS_RIGHT,
+ new HashMap<Integer, String>() {{
+ put(0, "Item (2)");
+ put(1, "Item (3)");
+ put(2, "Item (4)");
+ }});
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusRight_vertical_withoutAvailableTarget()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android version.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, VERTICAL);
+ /*
+ This generates the following grid:
+ 1 2 3
+ 4 5
+ */
+ runScrollInDirectionOnMultipleItemsAndFail(uiAutomation, View.FOCUS_RIGHT,
+ Collections.singletonList(4));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusRight_horizontal_withAvailableTarget()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android versions.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, HORIZONTAL);
+ /*
+ This generates the following grid:
+ 1 4
+ 2 5
+ 3
+ */
+ runScrollInDirectionOnMultipleItemsAndSucceed(uiAutomation, View.FOCUS_RIGHT,
+ new HashMap<Integer, String>() {{
+ put(0, "Item (4)");
+ put(1, "Item (5)");
+ put(3, "Item (2)");
+ put(4, "Item (3)");
+ }});
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusRight_horizontal_withoutAvailableTarget()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android versions.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, HORIZONTAL);
+ /*
+ This generates the following grid:
+ 1 4
+ 2 5
+ 3
+ */
+ runScrollInDirectionOnMultipleItemsAndFail(uiAutomation, View.FOCUS_RIGHT,
+ Collections.singletonList(2));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusLeft_vertical_withAvailableTarget()
+ throws Throwable {
+
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android version.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, VERTICAL);
+ /*
+ This generates the following grid:
+ 1 2 3
+ 4
+ */
+ runScrollInDirectionOnMultipleItemsAndSucceed(uiAutomation, View.FOCUS_LEFT,
+ new HashMap<Integer, String>() {{
+ put(1, "Item (1)");
+ put(2, "Item (2)");
+ put(3, "Item (3)");
+ }});
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusLeft_vertical_withoutAvailableTarget()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android version.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, VERTICAL);
+ /*
+ This generates the following grid:
+ 1 2 3
+ 4
+ */
+ runScrollInDirectionOnMultipleItemsAndFail(uiAutomation, View.FOCUS_LEFT,
+ Collections.singletonList(0));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusLeft_horizontal_withAvailableTarget()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android versions.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, HORIZONTAL);
+ /*
+ This generates the following grid:
+ 1 4
+ 2
+ 3
+ */
+ runScrollInDirectionOnMultipleItemsAndSucceed(uiAutomation, View.FOCUS_LEFT,
+ new HashMap<Integer, String>() {{
+ put(1, "Item (4)");
+ put(2, "Item (2)");
+ put(3, "Item (1)");
+ }});
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusLeft_horizontal_withoutAvailableTarget()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android versions.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, HORIZONTAL);
+ /*
+ This generates the following grid:
+ 1 4
+ 2
+ 3
+ */
+ runScrollInDirectionOnMultipleItemsAndFail(uiAutomation, View.FOCUS_LEFT,
+ Collections.singletonList(0));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusUp_vertical_withAvailableTarget()
+ throws Throwable {
+
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android version.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, VERTICAL);
+ /*
+ This generates the following grid:
+ 1 2 3
+ 4 5
+ */
+ runScrollInDirectionOnMultipleItemsAndSucceed(uiAutomation, View.FOCUS_UP,
+ new HashMap<Integer, String>() {{
+ put(3, "Item (1)");
+ put(4, "Item (2)");
+ }});
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusUp_vertical_withoutAvailableTarget()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android version.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, VERTICAL);
+ /*
+ This generates the following grid:
+ 1 2 3
+ 4 5
+ */
+ runScrollInDirectionOnMultipleItemsAndFail(uiAutomation, View.FOCUS_UP,
+ Arrays.asList(0, 1, 2));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusUp_horizontal_withAvailableTarget()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android versions.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, HORIZONTAL);
+ /*
+ This generates the following grid:
+ 1 4
+ 2
+ 3
+ */
+ runScrollInDirectionOnMultipleItemsAndSucceed(uiAutomation, View.FOCUS_UP,
+ new HashMap<Integer, String>() {{
+ put(1, "Item (1)");
+ put(2, "Item (2)");
+ }});
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusUp_horizontal_withoutAvailableTarget()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android versions.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, HORIZONTAL);
+ /*
+ This generates the following grid:
+ 1 4
+ 2
+ 3
+ */
+ runScrollInDirectionOnMultipleItemsAndFail(uiAutomation, View.FOCUS_UP,
+ Arrays.asList(0, 3));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusDown_vertical_withAvailableTarget()
+ throws Throwable {
+
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android version.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, VERTICAL);
+ /*
+ This generates the following grid:
+ 1 2 3
+ 4 5
+ */
+ runScrollInDirectionOnMultipleItemsAndSucceed(uiAutomation, View.FOCUS_DOWN,
+ new HashMap<Integer, String>() {{
+ put(0, "Item (4)");
+ put(1, "Item (5)");
+ }});
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusDown_vertical_withoutAvailableTarget()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android version.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, VERTICAL);
+ /*
+ This generates the following grid:
+ 1 2 3
+ 4 5
+ */
+ runScrollInDirectionOnMultipleItemsAndFail(uiAutomation, View.FOCUS_DOWN,
+ Arrays.asList(2, 3, 4));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusDown_horizontal_withAvailableTarget()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android versions.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, HORIZONTAL);
+ /*
+ This generates the following grid:
+ 1 4
+ 2
+ 3
+ */
+ runScrollInDirectionOnMultipleItemsAndSucceed(uiAutomation, View.FOCUS_DOWN,
+ new HashMap<Integer, String>() {{
+ put(0, "Item (2)");
+ put(1, "Item (3)");
+ }});
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusDown_horizontal_withoutAvailableTarget()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android versions.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, HORIZONTAL);
+ /*
+ This generates the following grid:
+ 1 4
+ 2
+ 3
+ */
+ runScrollInDirectionOnMultipleItemsAndFail(uiAutomation, View.FOCUS_DOWN,
+ Arrays.asList(2, 3));
+ }
+
+ /**
+ * Batch version of {@code runScrollInDirectionAndSucceed}. Sets accessibility focus on each
+ * grid child whose index is a key in {@code startingIndexToScrollTargetTextMap} and then runs
+ * {@code runScrollInDirectionAndSucceed} in the specified {@code direction}.
+ *
+ * @param uiAutomation UiAutomation instance.
+ * @param direction The direction of the scroll.
+ * @param startingIndexToScrollTargetTextMap Map where each key is the index of a grid
+ * child and the corresponding value is the text
+ * of the view targeted by the scroll.
+ * @throws TimeoutException Exception thrown when an action times out.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ private void runScrollInDirectionOnMultipleItemsAndSucceed(UiAutomation uiAutomation,
+ int direction, Map<Integer, String> startingIndexToScrollTargetTextMap)
+ throws TimeoutException {
+ for (Map.Entry<Integer, String> entry : startingIndexToScrollTargetTextMap.entrySet()) {
+ setAccessibilityFocus(uiAutomation, mGlm.getChildAt(entry.getKey()));
+ runScrollInDirectionAndSucceed(uiAutomation, direction, entry.getValue());
+ }
+ }
+
+ /**
+ * Verifies that a scroll successfully occurs in the specified {@code direction}.
+ *
+ * @param uiAutomation UiAutomation instance.
+ * @param direction The direction of the scroll.
+ * @param scrollTargetText The text of the view targeted by the scroll.
+ * @throws TimeoutException Exception thrown when an action times out.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ private void runScrollInDirectionAndSucceed(UiAutomation uiAutomation, int direction,
+ String scrollTargetText)
+ throws TimeoutException {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android versions.
+
+ final boolean[] returnValue = {false};
+ AccessibilityEvent awaitedEvent = uiAutomation.executeAndWaitForEvent(
+ () -> mActivityRule.runOnUiThread(() -> {
+ returnValue[0] =
+ mRecyclerView.getLayoutManager().performAccessibilityAction(
+ ACTION_SCROLL_IN_DIRECTION.getId(),
+ bundleWithDirectionArg(direction));
+ }),
+ event -> event.getEventType() == AccessibilityEvent.TYPE_VIEW_TARGETED_BY_SCROLL,
+ DEFAULT_ACCESSIBILITY_EVENT_TIMEOUT_MILLIS);
+
+ assertThat(scrollTargetText).isEqualTo(awaitedEvent.getSource().getText());
+ assertThat(returnValue[0]).isTrue();
+ }
+
+ /**
+ * Batch version of {@code runScrollInDirectionAndFail}. Sets accessibility focus on each
+ * grid child whose index is a key in {@code startingIndexToScrollTargetTextMap} and then runs
+ * {@code runScrollInDirectionAndFail}.
+ *
+ * @param uiAutomation UiAutomation instance.
+ * @param direction The direction of the scroll.
+ * @param startingIndices List where each item is the index of a grid child.
+ * @throws TimeoutException Exception thrown when an action times out.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ private void runScrollInDirectionOnMultipleItemsAndFail(UiAutomation uiAutomation,
+ int direction, List<Integer> startingIndices) throws TimeoutException {
+ for (Integer index: startingIndices) {
+ setAccessibilityFocus(uiAutomation, mGlm.getChildAt(index));
+ runScrollInDirectionAndFail(direction);
+ }
+ }
+
+ /**
+ * Verifies that a scroll does not occur in the specified {@code direction}.
+ *
+ * @param direction The direction of the scroll.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ private void runScrollInDirectionAndFail(int direction) {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android versions.
+
+ final boolean[] returnValue = {false};
+
+ mActivityRule.runOnUiThread(
+ () -> {
+ returnValue[0] = mRecyclerView.getLayoutManager().performAccessibilityAction(
+ ACTION_SCROLL_IN_DIRECTION.getId(), bundleWithDirectionArg(direction));
+ });
+
+ assertThat(returnValue[0]).isFalse();
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @NonNull
+ private UiAutomation setUpGridLayoutManagerAccessibilityTest(int itemCount, int orientation)
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android versions.
+
+ final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
+ setUpRecyclerViewAndGridLayoutManager(itemCount, orientation);
+ waitForFirstLayout(mRecyclerView);
+ return uiAutomation;
+ }
+
+ private Bundle bundleWithDirectionArg(int direction) {
+ Bundle bundle = new Bundle();
+ bundle.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_DIRECTION_INT, direction);
+ return bundle;
+ }
+
+ @NonNull
+ @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+ private UiAutomation setUpAndReturnUiAutomation() {
+ UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
+ final AccessibilityServiceInfo info = uiAutomation.getServiceInfo();
+ info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
+ uiAutomation.setServiceInfo(info);
+ return uiAutomation;
+ }
+
+ private void setAccessibilityFocus(UiAutomation uiAutomation, View source)
+ throws TimeoutException {
+ AccessibilityEvent awaitedEvent = null;
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ awaitedEvent = uiAutomation.executeAndWaitForEvent(
+ () -> {
+ try {
+ mActivityRule.runOnUiThread(() -> source.performAccessibilityAction(
+ ACTION_ACCESSIBILITY_FOCUS.getId(), null));
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ }
+ },
+ event -> event.getEventType()
+ == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED,
+ DEFAULT_ACCESSIBILITY_EVENT_TIMEOUT_MILLIS);
+ assertThat(awaitedEvent.getSource().isAccessibilityFocused()).isTrue();
+ }
+ }
+
+ private void setUpRecyclerViewAndGridLayoutManager(int itemCount, int orientation)
+ throws Throwable {
+ mRecyclerView = setupBasic(new Config(3, itemCount));
+ mGlm.setOrientation(orientation);
+ }
+
public GridLayoutManager.LayoutParams ensureGridLp(View view) {
ViewGroup.LayoutParams lp = view.getLayoutParams();
GridLayoutManager.LayoutParams glp;
@@ -1638,7 +2149,7 @@
rv.setLayoutParams(new ViewGroup.LayoutParams(500, 500));
mAdapter.setFullSpan(0);
waitForFirstLayout(rv);
- Truth.assertThat(getPositionToSpanIndexMapping()).containsExactly(
+ assertThat(getPositionToSpanIndexMapping()).containsExactly(
0, 0,
1, 0,
2, 1,
@@ -1656,7 +2167,7 @@
}
});
waitForAnimations(10);
- Truth.assertThat(getPositionToSpanIndexMapping()).containsExactly(
+ assertThat(getPositionToSpanIndexMapping()).containsExactly(
0, 0,
1, 0,
2, 1,
@@ -1669,7 +2180,7 @@
// 3 4
// 5 6
// 7
- Truth.assertThat(getPositionToSpanIndexMapping()).containsExactly(
+ assertThat(getPositionToSpanIndexMapping()).containsExactly(
3, 0,
4, 1,
5, 0,
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
index f043bf6..771c331 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
@@ -17,19 +17,29 @@
import android.content.Context;
import android.graphics.Rect;
+import android.os.Build;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.GridView;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
/**
* A {@link RecyclerView.LayoutManager} implementations that lays out items in a grid.
@@ -42,6 +52,7 @@
private static final boolean DEBUG = false;
private static final String TAG = "GridLayoutManager";
public static final int DEFAULT_SPAN_COUNT = -1;
+ private static final int INVALID_POSITION = -1;
/**
* Span size have been changed but we've not done a new layout calculation.
@@ -67,6 +78,13 @@
private boolean mUsingSpansToEstimateScrollBarDimensions;
/**
+ * Used to track the position of the target node brought on screen by
+ * {@code ACTIONS_SCROLL_IN_DIRECTION} so that a {@code TYPE_VIEW_TARGETED_BY_SCROLL} event can
+ * be emitted.
+ */
+ private int mPositionTargetedByScrollInDirection = INVALID_POSITION;
+
+ /**
* Constructor used when layout manager is set in XML by RecyclerView attribute
* "layoutManager". If spanCount is not specified in the XML, it defaults to a
* single column.
@@ -179,7 +197,94 @@
@Override
boolean performAccessibilityAction(int action, @Nullable Bundle args) {
- if (action == android.R.id.accessibilityActionScrollToPosition) {
+ // TODO (267511848): when U constants are finalized:
+ // - convert if/else blocks to switch statement
+ // - remove SDK check
+ // - remove the -1 check (this check makes accessibilityActionScrollInDirection
+ // no-op for < 34; see action definition in AccessibilityNodeInfoCompat.java).
+ if (action == AccessibilityActionCompat.ACTION_SCROLL_IN_DIRECTION.getId()
+ && action != -1) {
+ final View viewWithAccessibilityFocus = findChildWithAccessibilityFocus();
+ if (viewWithAccessibilityFocus == null) {
+ // TODO(b/268487724#comment2): handle rare cases when the requesting service does
+ // not place accessibility focus on a child. Consider scrolling forward/backward?
+ return false;
+ }
+
+ // Direction must be specified.
+ if (args == null) {
+ return false;
+ }
+
+ final int direction = args.getInt(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_DIRECTION_INT, INVALID_POSITION);
+
+ RecyclerView.ViewHolder vh =
+ mRecyclerView.getChildViewHolder(viewWithAccessibilityFocus);
+ if (vh == null) {
+ if (DEBUG) {
+ throw new RuntimeException(
+ "viewHolder is null for " + viewWithAccessibilityFocus);
+ }
+ return false;
+ }
+
+ int startingAdapterPosition = vh.getAbsoluteAdapterPosition();
+ int startingRow = getRowIndex(startingAdapterPosition);
+ int startingColumn = getColumnIndex(startingAdapterPosition);
+
+ if (startingRow < 0 || startingColumn < 0) {
+ if (DEBUG) {
+ throw new RuntimeException("startingRow equals " + startingRow + ", and "
+ + "startingColumn equals " + startingColumn + ", and neither can be "
+ + "less than 0.");
+ }
+ return false;
+ }
+
+ int scrollTargetPosition;
+
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ scrollTargetPosition = findScrollTargetPositionOnTheLeft(startingRow,
+ startingColumn, startingAdapterPosition);
+ break;
+ case View.FOCUS_RIGHT:
+ scrollTargetPosition = findScrollTargetPositionOnTheRight(startingRow,
+ startingColumn, startingAdapterPosition);
+ break;
+ case View.FOCUS_UP:
+ scrollTargetPosition = findScrollTargetPositionAbove(startingRow,
+ startingColumn, startingAdapterPosition);
+ break;
+ case View.FOCUS_DOWN:
+ scrollTargetPosition = findScrollTargetPositionBelow(startingRow,
+ startingColumn, startingAdapterPosition);
+ break;
+ default:
+ return false;
+ }
+
+ if (scrollTargetPosition == INVALID_POSITION
+ && mOrientation == RecyclerView.HORIZONTAL) {
+ // TODO (b/268487724): handle RTL.
+ // Handle case in grids with horizontal orientation where the scroll target is on
+ // a different row.
+ if (direction == View.FOCUS_LEFT) {
+ scrollTargetPosition = findPositionOfLastItemOnARowAbove(startingRow);
+ } else if (direction == View.FOCUS_RIGHT) {
+ scrollTargetPosition = findPositionOfFirstItemOnARowBelow(startingRow);
+ }
+ }
+
+ if (scrollTargetPosition != INVALID_POSITION) {
+ scrollToPosition(scrollTargetPosition);
+ mPositionTargetedByScrollInDirection = scrollTargetPosition;
+ return true;
+ }
+
+ return false;
+ } else if (action == android.R.id.accessibilityActionScrollToPosition) {
final int noRow = -1;
final int noColumn = -1;
if (args != null) {
@@ -228,6 +333,252 @@
return super.performAccessibilityAction(action, args);
}
+ private int findScrollTargetPositionOnTheRight(int startingRow, int startingColumn,
+ int startingAdapterPosition) {
+ int scrollTargetPosition = INVALID_POSITION;
+ for (int i = startingAdapterPosition + 1; i < getItemCount(); i++) {
+ int currentRow = getRowIndex(i);
+ int currentColumn = getColumnIndex(i);
+
+ if (currentRow < 0 || currentColumn < 0) {
+ if (DEBUG) {
+ throw new RuntimeException("currentRow equals " + currentRow + ", and "
+ + "currentColumn equals " + currentColumn + ", and neither can be "
+ + "less than 0.");
+ }
+ return INVALID_POSITION;
+ }
+
+ // Canonical case: target is on the same row. TODO (b/268487724): handle RTL.
+ if (currentRow == startingRow && currentColumn > startingColumn) {
+ return i;
+ } else {
+ if (mOrientation == VERTICAL) {
+ /*
+ * Grids with vertical layouts are laid out row by row...
+ * 1 2 3
+ * 4 5 6
+ * 7 8
+ * ... and the scroll target may lie on a following row.
+ */
+ if (currentRow > startingRow) {
+ scrollTargetPosition = i;
+ break;
+ }
+ } else { // HORIZONTAL
+ // TODO (b/268487724): handle case where the scroll target spans multiple
+ // rows/columns.
+ }
+ }
+ }
+ return scrollTargetPosition;
+ }
+
+ private int findScrollTargetPositionOnTheLeft(int startingRow, int startingColumn,
+ int startingAdapterPosition) {
+ int scrollTargetPosition = INVALID_POSITION;
+ for (int i = startingAdapterPosition - 1; i >= 0; i--) {
+ int currentRow = getRowIndex(i);
+ int currentColumn = getColumnIndex(i);
+
+ if (currentRow < 0 || currentColumn < 0) {
+ if (DEBUG) {
+ throw new RuntimeException("currentRow equals " + currentRow + ", and "
+ + "currentColumn equals " + currentColumn + ", and neither can be "
+ + "less than 0.");
+ }
+ return INVALID_POSITION;
+ }
+
+ // Canonical case: target is on the same row. TODO (b/268487724): handle RTL.
+ if (currentRow == startingRow && currentColumn < startingColumn) {
+ return i;
+ } else {
+ if (mOrientation == VERTICAL) {
+ /*
+ * Grids with vertical layouts are laid out row by row...
+ * 1 2 3
+ * 4 5 6
+ * 7 8
+ * ... and the scroll target may lie on a preceding row.
+ */
+ if (currentRow < startingRow) {
+ scrollTargetPosition = i;
+ break;
+ }
+ } else { // HORIZONTAL
+ // TODO (b/268487724): handle case where the scroll target spans multiple
+ // rows/columns.
+ }
+ }
+ }
+ return scrollTargetPosition;
+ }
+
+ private int findScrollTargetPositionAbove(int startingRow, int startingColumn,
+ int startingAdapterPosition) {
+ int scrollTargetPosition = INVALID_POSITION;
+ for (int i = startingAdapterPosition - 1; i >= 0; i--) {
+ int currentRow = getRowIndex(i);
+ int currentColumn = getColumnIndex(i);
+
+ if (currentRow < 0 || currentColumn < 0) {
+ if (DEBUG) {
+ throw new RuntimeException("currentRow equals " + currentRow + ", and "
+ + "currentColumn equals " + currentColumn + ", and neither can be "
+ + "less than 0.");
+ }
+ return INVALID_POSITION;
+ }
+
+ if (currentRow < startingRow && currentColumn == startingColumn) {
+ scrollTargetPosition = i;
+ break;
+ }
+ }
+ return scrollTargetPosition;
+ }
+
+ private int findScrollTargetPositionBelow(int startingRow, int startingColumn,
+ int startingAdapterPosition) {
+ int scrollTargetPosition = INVALID_POSITION;
+ for (int i = startingAdapterPosition + 1; i < getItemCount(); i++) {
+ int currentRow = getRowIndex(i);
+ int currentColumn = getColumnIndex(i);
+
+ if (currentRow < 0 || currentColumn < 0) {
+ if (DEBUG) {
+ throw new RuntimeException("currentRow equals " + currentRow + ", and "
+ + "currentColumn equals " + currentColumn + ", and neither can be "
+ + "less than 0.");
+ }
+ return INVALID_POSITION;
+ }
+
+ if (currentRow > startingRow && currentColumn == startingColumn) {
+ scrollTargetPosition = i;
+ break;
+ }
+ }
+ return scrollTargetPosition;
+ }
+
+ @SuppressWarnings("ConstantConditions") // For the spurious NPE warning related to getting a
+ // value from a map using one of the map keys.
+ int findPositionOfLastItemOnARowAbove(int startingRow) {
+ if (startingRow < 0) {
+ if (DEBUG) {
+ throw new RuntimeException(
+ "startingRow equals " + startingRow + ". It cannot be less than zero");
+ }
+ return INVALID_POSITION;
+ }
+
+ // Map where the keys are row numbers and values are the adapter positions of the last
+ // item in each row. This map is used to locate a scroll target on a previous row in grids
+ // with horizontal orientation. In this example...
+ // 1 4 7
+ // 2 5 8
+ // 3 6
+ // ... the generated map - {2 -> 5, 1 -> 7, 0 -> 6} - can be used to scroll from,
+ // say, "2" (adapter position 1) in the second row to "7" (adapter position 6) in the
+ // preceding row.
+ Map<Integer, Integer> rowToLastItemPositionMap = new TreeMap<>(Collections.reverseOrder());
+ for (int position = 0; position < getItemCount(); position++) {
+ int row = getRowIndex(position);
+ if (row < 0) {
+ if (DEBUG) {
+ throw new RuntimeException(
+ "row equals " + row + ". It cannot be less than zero");
+ }
+ return INVALID_POSITION;
+ }
+ rowToLastItemPositionMap.put(row, position);
+ }
+
+ for (int row : rowToLastItemPositionMap.keySet()) {
+ if (row < startingRow) {
+ return rowToLastItemPositionMap.get(row);
+ }
+ }
+ return INVALID_POSITION;
+ }
+
+ @SuppressWarnings("ConstantConditions") // For the spurious NPE warning related to getting a
+ // value from a map using one of the map keys.
+ int findPositionOfFirstItemOnARowBelow(int startingRow) {
+ if (startingRow < 0) {
+ if (DEBUG) {
+ throw new RuntimeException(
+ "startingRow equals " + startingRow + ". It cannot be less than zero");
+ }
+ return INVALID_POSITION;
+ }
+
+ // Map where the keys are row numbers and values are the adapter positions of the first
+ // item in each row. This map is used to locate a scroll target on a following row in grids
+ // with horizontal orientation. In this example:
+ // 1 4 7
+ // 2 5 8
+ // 3 6
+ // ... the generated map - {0 -> 0, 1 -> 1, 2 -> 2} - can be used to scroll from, say,
+ // "7" (adapter position 6) in the first row to "2" (adapter position 1) in the next row.
+ Map<Integer, Integer> rowToFirstItemPositionMap = new TreeMap<>();
+ for (int position = 0; position < getItemCount(); position++) {
+ int row = getRowIndex(position);
+ if (row < 0) {
+ if (DEBUG) {
+ throw new RuntimeException(
+ "row equals " + row + ". It cannot be less than zero");
+ }
+ return INVALID_POSITION;
+ }
+
+ if (!rowToFirstItemPositionMap.containsKey(row)) {
+ rowToFirstItemPositionMap.put(row, position);
+ }
+ }
+
+ for (int row : rowToFirstItemPositionMap.keySet()) {
+ if (row > startingRow) {
+ return rowToFirstItemPositionMap.get(row);
+ }
+ }
+ return INVALID_POSITION;
+ }
+
+ private int getRowIndex(int position) {
+ return mOrientation == VERTICAL ? getSpanGroupIndex(mRecyclerView.mRecycler,
+ mRecyclerView.mState, position) : getSpanIndex(mRecyclerView.mRecycler,
+ mRecyclerView.mState, position);
+ }
+
+ private int getColumnIndex(int position) {
+ return mOrientation == HORIZONTAL ? getSpanGroupIndex(mRecyclerView.mRecycler,
+ mRecyclerView.mState, position) : getSpanIndex(mRecyclerView.mRecycler,
+ mRecyclerView.mState, position);
+ }
+
+ @Nullable
+ private View findChildWithAccessibilityFocus() {
+ View child = null;
+ // SDK check needed for View#isAccessibilityFocused()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ boolean childFound = false;
+ int i;
+ for (i = 0; i < getChildCount(); i++) {
+ if (Api21Impl.isAccessibilityFocused(Objects.requireNonNull(getChildAt(i)))) {
+ childFound = true;
+ break;
+ }
+ }
+ if (childFound) {
+ child = getChildAt(i);
+ }
+ }
+ return child;
+ }
+
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (state.isPreLayout()) {
@@ -244,6 +595,19 @@
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
mPendingSpanCountChange = false;
+ if (mPositionTargetedByScrollInDirection != INVALID_POSITION) {
+ View viewTargetedByScrollInDirection = findViewByPosition(
+ mPositionTargetedByScrollInDirection);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
+ && viewTargetedByScrollInDirection != null) {
+ // Send event after the scroll associated with ACTION_SCROLL_IN_DIRECTION (see
+ // performAccessibilityAction()) concludes and layout completes. Accessibility
+ // services can listen for this event and change UI state as needed.
+ viewTargetedByScrollInDirection.sendAccessibilityEvent(
+ AccessibilityEvent.TYPE_VIEW_TARGETED_BY_SCROLL);
+ mPositionTargetedByScrollInDirection = INVALID_POSITION;
+ }
+ }
}
private void clearPreLayoutSpanMappingCache() {
@@ -1506,4 +1870,17 @@
return mSpanSize;
}
}
+
+
+ @RequiresApi(21)
+ private static class Api21Impl {
+ private Api21Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static boolean isAccessibilityFocused(@NonNull View view) {
+ return view.isAccessibilityFocused();
+ }
+ }
}
\ No newline at end of file
diff --git a/resourceinspection/resourceinspection-annotation/build.gradle b/resourceinspection/resourceinspection-annotation/build.gradle
index 86bb07a..a5b9ce9 100644
--- a/resourceinspection/resourceinspection-annotation/build.gradle
+++ b/resourceinspection/resourceinspection-annotation/build.gradle
@@ -26,7 +26,7 @@
}
androidx {
- name = "Android Resource Inspection - Annotations"
+ name = "Resource Inspection - Annotations"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2021"
description = "Annotation processors for Android resource and layout inspection"
diff --git a/resourceinspection/resourceinspection-processor/build.gradle b/resourceinspection/resourceinspection-processor/build.gradle
index a633319..959ea3d 100644
--- a/resourceinspection/resourceinspection-processor/build.gradle
+++ b/resourceinspection/resourceinspection-processor/build.gradle
@@ -50,7 +50,7 @@
}
androidx {
- name = "Android Resource Inspection - Annotation Processor"
+ name = "Resource Inspection - Annotation Processor"
type = LibraryType.ANNOTATION_PROCESSOR
inceptionYear = "2021"
description = "Annotation processors for Android resource and layout inspection"
diff --git a/room/room-common/build.gradle b/room/room-common/build.gradle
index 151639e..bd514ec 100644
--- a/room/room-common/build.gradle
+++ b/room/room-common/build.gradle
@@ -32,7 +32,7 @@
}
androidx {
- name = "Android Room-Common"
+ name = "Room-Common"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Room-Common"
diff --git a/room/room-compiler-processing-testing/build.gradle b/room/room-compiler-processing-testing/build.gradle
index 2d1f1e7c..abc8872 100644
--- a/room/room-compiler-processing-testing/build.gradle
+++ b/room/room-compiler-processing-testing/build.gradle
@@ -72,7 +72,7 @@
}
androidx {
- name = "AndroidX Room XProcessor Testing"
+ name = "Room XProcessor Testing"
type = LibraryType.ANNOTATION_PROCESSOR_UTILS
inceptionYear = "2020"
description = "Testing helpers for Room XProcessing APIs"
diff --git a/room/room-compiler-processing/build.gradle b/room/room-compiler-processing/build.gradle
index f3cabd0..f8d3190 100644
--- a/room/room-compiler-processing/build.gradle
+++ b/room/room-compiler-processing/build.gradle
@@ -98,7 +98,7 @@
}
androidx {
- name = "AndroidX Room XProcessor"
+ name = "Room XProcessor"
type = LibraryType.ANNOTATION_PROCESSOR_UTILS
inceptionYear = "2020"
description = "Processing Environment Abstraction for AndroidX Room"
diff --git a/room/room-compiler/build.gradle b/room/room-compiler/build.gradle
index 6b7096e..98470ed 100644
--- a/room/room-compiler/build.gradle
+++ b/room/room-compiler/build.gradle
@@ -299,7 +299,7 @@
}
androidx {
- name = "Android Room Compiler"
+ name = "Room Compiler"
type = LibraryType.ANNOTATION_PROCESSOR
inceptionYear = "2017"
description = "Android Room annotation processor"
diff --git a/room/room-gradle-plugin/build.gradle b/room/room-gradle-plugin/build.gradle
index 8b12c66..e9dfec6 100644
--- a/room/room-gradle-plugin/build.gradle
+++ b/room/room-gradle-plugin/build.gradle
@@ -81,7 +81,7 @@
}
androidx {
- name = "Android Room Gradle Plugin"
+ name = "Room Gradle Plugin"
type = LibraryType.GRADLE_PLUGIN
inceptionYear = "2023"
description = "Android Room Gradle Plugin"
diff --git a/room/room-guava/build.gradle b/room/room-guava/build.gradle
index 99c5ad7..572953f 100644
--- a/room/room-guava/build.gradle
+++ b/room/room-guava/build.gradle
@@ -34,7 +34,7 @@
}
androidx {
- name = "Android Room Guava"
+ name = "Room Guava"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Android Room Guava"
diff --git a/room/room-ktx/build.gradle b/room/room-ktx/build.gradle
index 5e4a868..3dfa666 100644
--- a/room/room-ktx/build.gradle
+++ b/room/room-ktx/build.gradle
@@ -41,7 +41,7 @@
}
androidx {
- name = "Android Room Kotlin Extensions"
+ name = "Room Kotlin Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
description = "Android Room Kotlin Extensions"
diff --git a/room/room-migration/build.gradle b/room/room-migration/build.gradle
index 20954b0..3eb7477 100644
--- a/room/room-migration/build.gradle
+++ b/room/room-migration/build.gradle
@@ -32,7 +32,7 @@
}
androidx {
- name = "Android Room Migration"
+ name = "Room Migration"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Room Migration"
diff --git a/room/room-runtime-lint/build.gradle b/room/room-runtime-lint/build.gradle
index ea43587..52cc23c 100644
--- a/room/room-runtime-lint/build.gradle
+++ b/room/room-runtime-lint/build.gradle
@@ -33,7 +33,7 @@
}
androidx {
- name = "Android Room-Runtime Lint Checks"
+ name = "Room-Runtime Lint Checks"
type = LibraryType.LINT
inceptionYear = "2022"
description = "Android Room-Runtime Lint Checks"
diff --git a/room/room-runtime/build.gradle b/room/room-runtime/build.gradle
index 5859784..f5a047d 100644
--- a/room/room-runtime/build.gradle
+++ b/room/room-runtime/build.gradle
@@ -73,7 +73,7 @@
}
androidx {
- name = "Android Room-Runtime"
+ name = "Room-Runtime"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Room-Runtime"
diff --git a/room/room-rxjava2/build.gradle b/room/room-rxjava2/build.gradle
index 6cc2e1e..c802802 100644
--- a/room/room-rxjava2/build.gradle
+++ b/room/room-rxjava2/build.gradle
@@ -39,7 +39,7 @@
}
androidx {
- name = "Android Room RXJava2"
+ name = "Room RXJava2"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Room RXJava2"
diff --git a/room/room-rxjava2/lint-baseline.xml b/room/room-rxjava2/lint-baseline.xml
index 02e92c5..656b9b3 100644
--- a/room/room-rxjava2/lint-baseline.xml
+++ b/room/room-rxjava2/lint-baseline.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.0.0-beta03" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-beta03)" variant="all" version="8.0.0-beta03">
-
<issue
id="UnknownNullness"
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
diff --git a/room/room-rxjava3/build.gradle b/room/room-rxjava3/build.gradle
index fee4abe..3533c33 100644
--- a/room/room-rxjava3/build.gradle
+++ b/room/room-rxjava3/build.gradle
@@ -40,7 +40,7 @@
}
androidx {
- name = "Android Room RXJava3"
+ name = "Room RXJava3"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2020"
description = "Android Room RXJava3"
diff --git a/room/room-testing/build.gradle b/room/room-testing/build.gradle
index 1471e49..e8970dc 100644
--- a/room/room-testing/build.gradle
+++ b/room/room-testing/build.gradle
@@ -54,7 +54,7 @@
}
androidx {
- name = "Android Room Testing"
+ name = "Room Testing"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Room Testing"
diff --git a/samples/MediaRoutingDemo/src/main/AndroidManifest.xml b/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
index b56a49f..eb9d733 100644
--- a/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
+++ b/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
@@ -37,6 +37,7 @@
android:label="@string/main_activity_label">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
+ <action android:name="android.media.action.TRANSFER_MEDIA"/>
<category android:name="com.example.androidx.SAMPLE_CODE" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -64,6 +65,17 @@
</intent-filter>
</activity>
+ <activity
+ android:name=".activities.RouteListingPreferenceActivity"
+ android:configChanges="orientation|screenSize"
+ android:exported="false"
+ android:label="Route Listing Preference">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="com.example.androidx.SAMPLE_CODE" />
+ </intent-filter>
+ </activity>
+
<receiver android:name="androidx.mediarouter.media.MediaTransferReceiver"
android:exported="true" />
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/RoutesManager.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/RoutesManager.java
index 7e69d77..f4d1b6f 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/RoutesManager.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/RoutesManager.java
@@ -23,17 +23,23 @@
import static com.example.androidx.mediarouting.data.RouteItem.PlaybackType.REMOTE;
import static com.example.androidx.mediarouting.data.RouteItem.VolumeHandling.VARIABLE;
+import android.content.ComponentName;
import android.content.Context;
import android.content.res.Resources;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.core.os.BuildCompat;
import androidx.mediarouter.media.MediaRouter;
import androidx.mediarouter.media.MediaRouterParams;
+import androidx.mediarouter.media.RouteListingPreference;
+import com.example.androidx.mediarouting.activities.MainActivity;
import com.example.androidx.mediarouting.data.RouteItem;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -42,6 +48,7 @@
public final class RoutesManager {
private static final String VARIABLE_VOLUME_BASIC_ROUTE_ID = "variable_basic";
+ private static final String SENDER_DRIVEN_BASIC_ROUTE_ID = "sender_driven_route";
private static final int VOLUME_MAX = 25;
private static final int VOLUME_DEFAULT = 5;
@@ -51,12 +58,18 @@
private final Map<String, RouteItem> mRouteItems;
private boolean mDynamicRoutingEnabled;
private DialogType mDialogType;
+ private final MediaRouter mMediaRouter;
+ private boolean mRouteListingPreferenceEnabled;
+ private boolean mRouteListingSystemOrderingPreferred;
+ private List<RouteListingPreferenceItemHolder> mRouteListingPreferenceItems;
private RoutesManager(Context context) {
mContext = context;
mDynamicRoutingEnabled = true;
mDialogType = DialogType.OUTPUT_SWITCHER;
mRouteItems = new HashMap<>();
+ mRouteListingPreferenceItems = Collections.emptyList();
+ mMediaRouter = MediaRouter.getInstance(context);
initTestRoutes();
}
@@ -113,6 +126,76 @@
mRouteItems.put(routeItem.getId(), routeItem);
}
+ /**
+ * Returns whether route listing preference is enabled.
+ *
+ * @see #setRouteListingPreferenceEnabled
+ */
+ public boolean isRouteListingPreferenceEnabled() {
+ return mRouteListingPreferenceEnabled;
+ }
+
+ /**
+ * Sets whether the use of route listing preference is enabled or not.
+ *
+ * <p>If route listing preference is enabled, the route listing preference configuration for
+ * this app is maintained following the item list provided via {@link
+ * #setRouteListingPreferenceItems}. Otherwise, if route listing preference is disabled, the
+ * route listing preference for this app is set to null.
+ *
+ * <p>Does not affect the system's state if called on a device running API 33 or older.
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ public void setRouteListingPreferenceEnabled(boolean routeListingPreferenceEnabled) {
+ mRouteListingPreferenceEnabled = routeListingPreferenceEnabled;
+ onRouteListingPreferenceChanged();
+ }
+
+ /** Returns whether the system ordering for route listing is preferred. */
+ public boolean getRouteListingSystemOrderingPreferred() {
+ return mRouteListingSystemOrderingPreferred;
+ }
+
+ /**
+ * Sets whether to prefer the system ordering for route listing.
+ *
+ * <p>True means that the ordering for route listing is the one in the {@link #getRouteItems()}
+ * list. If false, the ordering of said list is ignored, and the system uses its builtin
+ * ordering for the items.
+ *
+ * <p>Does not affect the system's state if called on a device running API 33 or older.
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ public void setRouteListingSystemOrderingPreferred(
+ boolean routeListingSystemOrderringPreferred) {
+ mRouteListingSystemOrderingPreferred = routeListingSystemOrderringPreferred;
+ onRouteListingPreferenceChanged();
+ }
+
+ /**
+ * The current list of route listing preference items, as set via {@link
+ * #setRouteListingPreferenceItems}.
+ */
+ @NonNull
+ public List<RouteListingPreferenceItemHolder> getRouteListingPreferenceItems() {
+ return mRouteListingPreferenceItems;
+ }
+
+ /**
+ * Sets the route listing preference items.
+ *
+ * <p>Does not affect the system's state if called on a device running API 33 or older.
+ *
+ * @see #setRouteListingPreferenceEnabled
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ public void setRouteListingPreferenceItems(
+ @NonNull List<RouteListingPreferenceItemHolder> preference) {
+ mRouteListingPreferenceItems =
+ Collections.unmodifiableList(new ArrayList<>(preference));
+ onRouteListingPreferenceChanged();
+ }
+
/** Changes the media router dialog type with the type stored in {@link RoutesManager} */
public void reloadDialogType() {
MediaRouter mediaRouter = MediaRouter.getInstance(mContext.getApplicationContext());
@@ -191,10 +274,58 @@
r4.setVolume(VOLUME_DEFAULT);
r4.setCanDisconnect(true);
+ RouteItem r5 = new RouteItem();
+ r5.setId(SENDER_DRIVEN_BASIC_ROUTE_ID + "1");
+ r5.setName(r.getString(R.string.sender_driven_route_name1));
+ r5.setDescription(r.getString(R.string.sample_route_description));
+ r5.setControlFilter(BASIC);
+ r5.setDeviceType(TV);
+ r5.setPlaybackStream(MUSIC);
+ r5.setPlaybackType(REMOTE);
+ r5.setVolumeHandling(VARIABLE);
+ r5.setVolumeMax(VOLUME_MAX);
+ r5.setVolume(VOLUME_DEFAULT);
+ r5.setCanDisconnect(true);
+ r5.setSenderDriven(true);
+
+ RouteItem r6 = new RouteItem();
+ r6.setId(SENDER_DRIVEN_BASIC_ROUTE_ID + "2");
+ r6.setName(r.getString(R.string.sender_driven_route_name2));
+ r6.setDescription(r.getString(R.string.sample_route_description));
+ r6.setControlFilter(BASIC);
+ r6.setDeviceType(TV);
+ r6.setPlaybackStream(MUSIC);
+ r6.setPlaybackType(REMOTE);
+ r6.setVolumeHandling(VARIABLE);
+ r6.setVolumeMax(VOLUME_MAX);
+ r6.setVolume(VOLUME_DEFAULT);
+ r6.setCanDisconnect(true);
+ r6.setSenderDriven(true);
+
mRouteItems.put(r1.getId(), r1);
mRouteItems.put(r2.getId(), r2);
mRouteItems.put(r3.getId(), r3);
mRouteItems.put(r4.getId(), r4);
+ mRouteItems.put(r5.getId(), r5);
+ mRouteItems.put(r6.getId(), r6);
+ }
+
+ private void onRouteListingPreferenceChanged() {
+ RouteListingPreference routeListingPreference = null;
+ if (mRouteListingPreferenceEnabled) {
+ ArrayList<RouteListingPreference.Item> items = new ArrayList<>();
+ for (RouteListingPreferenceItemHolder item : mRouteListingPreferenceItems) {
+ items.add(item.mItem);
+ }
+ routeListingPreference =
+ new RouteListingPreference.Builder()
+ .setItems(items)
+ .setLinkedItemComponentName(
+ new ComponentName(mContext, MainActivity.class))
+ .setUseSystemOrdering(mRouteListingSystemOrderingPreferred)
+ .build();
+ }
+ mMediaRouter.setRouteListingPreference(routeListingPreference);
}
public enum DialogType {
@@ -202,4 +333,38 @@
DYNAMIC_GROUP,
OUTPUT_SWITCHER
}
+
+ /**
+ * Holds a {@link RouteListingPreference.Item} and the associated route's name.
+ *
+ * <p>Convenient pair-like class for populating UI elements, ensuring we have an associated
+ * route name for each route listing preference item even after the corresponding route no
+ * longer exists.
+ */
+ public static final class RouteListingPreferenceItemHolder {
+
+ @NonNull public final RouteListingPreference.Item mItem;
+ @NonNull public final String mRouteName;
+
+ public RouteListingPreferenceItemHolder(
+ @NonNull RouteListingPreference.Item item, @NonNull String routeName) {
+ mItem = item;
+ mRouteName = routeName;
+ }
+
+ /** Returns the name of the corresponding route. */
+ @Override
+ @NonNull
+ public String toString() {
+ return mRouteName;
+ }
+
+ /**
+ * Returns whether the contained {@link RouteListingPreference.Item} has the given {@code
+ * flag} set.
+ */
+ public boolean hasFlag(int flag) {
+ return (mItem.getFlags() & flag) == flag;
+ }
+ }
}
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 7741f9f..aed9f13 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
@@ -16,6 +16,8 @@
package com.example.androidx.mediarouting.activities;
+import static com.example.androidx.mediarouting.ui.UiUtils.setUpEnumBasedSpinner;
+
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -24,17 +26,14 @@
import android.os.IBinder;
import android.text.Editable;
import android.text.TextWatcher;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
-import android.widget.Spinner;
import android.widget.Switch;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.util.Consumer;
import com.example.androidx.mediarouting.R;
import com.example.androidx.mediarouting.RoutesManager;
@@ -49,7 +48,6 @@
private ServiceConnection mConnection;
private RoutesManager mRoutesManager;
private RouteItem mRouteItem;
- private Switch mCanDisconnectSwitch;
/** Launches the activity. */
public static void launchActivity(@NonNull Context context, @Nullable String routeId) {
@@ -75,8 +73,6 @@
mRouteItem = RouteItem.copyOf(mRouteItem);
}
- mCanDisconnectSwitch = findViewById(R.id.cam_disconnect_switch);
-
setUpViews();
}
@@ -149,19 +145,19 @@
String.valueOf(mRouteItem.getVolumeMax()),
mewVolumeMax -> mRouteItem.setVolumeMax(Integer.parseInt(mewVolumeMax)));
- setUpCanDisconnectSwitch();
+ setUpSwitch(
+ findViewById(R.id.can_disconnect_switch),
+ mRouteItem.isCanDisconnect(),
+ newValue -> mRouteItem.setCanDisconnect(newValue));
+
+ setUpSwitch(
+ findViewById(R.id.is_sender_driven_switch),
+ mRouteItem.isSenderDriven(),
+ newValue -> mRouteItem.setSenderDriven(newValue));
setUpSaveButton();
}
- private void setUpCanDisconnectSwitch() {
- mCanDisconnectSwitch.setChecked(mRouteItem.isCanDisconnect());
- mCanDisconnectSwitch.setOnCheckedChangeListener(
- (compoundButton, b) -> {
- mRouteItem.setCanDisconnect(b);
- });
- }
-
private void setUpSaveButton() {
Button saveButton = findViewById(R.id.save_button);
saveButton.setOnClickListener(
@@ -172,10 +168,14 @@
});
}
+ private static void setUpSwitch(Switch switchWidget, boolean currentValue,
+ Consumer<Boolean> propertySetter) {
+ switchWidget.setChecked(currentValue);
+ switchWidget.setOnCheckedChangeListener((compoundButton, b) -> propertySetter.accept(b));
+ }
+
private static void setUpEditText(
- EditText editText,
- String currentValue,
- RoutePropertySetter<String> routePropertySetter) {
+ EditText editText, String currentValue, Consumer<String> propertySetter) {
editText.setText(currentValue);
editText.addTextChangedListener(
new TextWatcher() {
@@ -185,7 +185,7 @@
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
- routePropertySetter.accept(charSequence.toString());
+ propertySetter.accept(charSequence.toString());
}
@Override
@@ -193,36 +193,6 @@
});
}
- private static void setUpEnumBasedSpinner(
- Context context,
- Spinner spinner,
- Enum<?> anEnum,
- RoutePropertySetter<Enum<?>> routePropertySetter) {
- Enum<?>[] enumValues = anEnum.getDeclaringClass().getEnumConstants();
- ArrayAdapter<Enum<?>> adapter =
- new ArrayAdapter<>(context, android.R.layout.simple_spinner_item, enumValues);
- adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
- spinner.setAdapter(adapter);
- spinner.setSelection(anEnum.ordinal());
-
- spinner.setOnItemSelectedListener(
- new AdapterView.OnItemSelectedListener() {
- @Override
- public void onItemSelected(
- AdapterView<?> adapterView, View view, int i, long l) {
- routePropertySetter.accept(
- anEnum.getDeclaringClass().getEnumConstants()[i]);
- }
-
- @Override
- public void onNothingSelected(AdapterView<?> adapterView) {}
- });
- }
-
- private interface RoutePropertySetter<T> {
- void accept(T value);
- }
-
private class ProviderServiceConnection implements ServiceConnection {
@Override
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 3558f3a..9247677 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
@@ -45,6 +45,7 @@
import android.widget.TabHost;
import android.widget.TabHost.TabSpec;
import android.widget.TextView;
+import android.widget.Toast;
import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
@@ -66,6 +67,7 @@
import androidx.mediarouter.media.MediaRouter.ProviderInfo;
import androidx.mediarouter.media.MediaRouter.RouteInfo;
import androidx.mediarouter.media.MediaRouterParams;
+import androidx.mediarouter.media.RouteListingPreference;
import com.example.androidx.mediarouting.MyMediaRouteControllerDialog;
import com.example.androidx.mediarouting.R;
@@ -82,6 +84,7 @@
import com.google.common.util.concurrent.ListenableFuture;
import java.io.File;
+import java.util.List;
/**
* Demonstrates how to use the {@link MediaRouter} API to build an application that allows the user
@@ -269,6 +272,10 @@
mSessionManager.setCallback(new SampleSessionManagerCallback());
updateUi();
+
+ if (RouteListingPreference.ACTION_TRANSFER_MEDIA.equals(getIntent().getAction())) {
+ showMediaTransferToast();
+ }
}
@Override
@@ -333,6 +340,26 @@
requestPostNotificationsPermission();
}
+ private void showMediaTransferToast() {
+ String routeId = getIntent().getStringExtra(RouteListingPreference.EXTRA_ROUTE_ID);
+ List<RouteInfo> routes = mMediaRouter.getRoutes();
+ String requestedRouteName = null;
+ for (RouteInfo route : routes) {
+ if (route.getId().equals(routeId)) {
+ requestedRouteName = route.getName();
+ break;
+ }
+ }
+ String stringToDisplay =
+ requestedRouteName != null
+ ? "Transfer requested to " + requestedRouteName
+ : "Transfer requested to unknown route: " + routeId;
+
+ // TODO(b/266561322): Replace the toast with a Dialog that allows the user to either
+ // transfer playback to the requested route, or dismiss the intent.
+ Toast.makeText(/* context= */ this, stringToDisplay, Toast.LENGTH_LONG).show();
+ }
+
private void requestDisplayOverOtherAppsPermission() {
// Need overlay permission for emulating remote display.
if (Build.VERSION.SDK_INT >= 23 && !Api23Impl.canDrawOverlays(this)) {
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/RouteListingPreferenceActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/RouteListingPreferenceActivity.java
new file mode 100644
index 0000000..308d3c8
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/RouteListingPreferenceActivity.java
@@ -0,0 +1,478 @@
+/*
+ * 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 com.example.androidx.mediarouting.activities;
+
+import static androidx.mediarouter.media.RouteListingPreference.Item.FLAG_ONGOING_SESSION;
+import static androidx.mediarouter.media.RouteListingPreference.Item.FLAG_ONGOING_SESSION_MANAGED;
+import static androidx.mediarouter.media.RouteListingPreference.Item.FLAG_SUGGESTED;
+import static androidx.mediarouter.media.RouteListingPreference.Item.SUBTEXT_CUSTOM;
+
+import android.os.Build;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.Spinner;
+import android.widget.Switch;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.os.BuildCompat;
+import androidx.mediarouter.media.MediaRouter;
+import androidx.mediarouter.media.RouteListingPreference;
+import androidx.recyclerview.widget.ItemTouchHelper;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.example.androidx.mediarouting.R;
+import com.example.androidx.mediarouting.RoutesManager;
+import com.example.androidx.mediarouting.RoutesManager.RouteListingPreferenceItemHolder;
+import com.example.androidx.mediarouting.ui.UiUtils;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** Allows the user to manage the route listing preference of this app. */
+public class RouteListingPreferenceActivity extends AppCompatActivity {
+
+ private RoutesManager mRoutesManager;
+ private RecyclerView mRouteListingPreferenceRecyclerView;
+
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (!BuildCompat.isAtLeastU()) {
+ Toast.makeText(
+ /* context= */ this,
+ "Route Listing Preference requires Android U+",
+ Toast.LENGTH_LONG)
+ .show();
+ finish();
+ return;
+ }
+
+ setContentView(R.layout.activity_route_listing_preference);
+
+ mRoutesManager = RoutesManager.getInstance(/* context= */ this);
+
+ Switch preferSystemOrderingSwitch = findViewById(R.id.prefer_system_ordering_switch);
+ preferSystemOrderingSwitch.setChecked(
+ mRoutesManager.getRouteListingSystemOrderingPreferred());
+ preferSystemOrderingSwitch.setOnCheckedChangeListener(
+ (unusedButton, isChecked) -> {
+ mRoutesManager.setRouteListingSystemOrderingPreferred(isChecked);
+ });
+ preferSystemOrderingSwitch.setEnabled(mRoutesManager.isRouteListingPreferenceEnabled());
+
+ Switch enableRouteListingPreferenceSwitch =
+ findViewById(R.id.enable_route_listing_preference_switch);
+ enableRouteListingPreferenceSwitch.setChecked(
+ mRoutesManager.isRouteListingPreferenceEnabled());
+ enableRouteListingPreferenceSwitch.setOnCheckedChangeListener(
+ (unusedButton, isChecked) -> {
+ mRoutesManager.setRouteListingPreferenceEnabled(isChecked);
+ preferSystemOrderingSwitch.setEnabled(isChecked);
+ });
+
+ mRouteListingPreferenceRecyclerView =
+ findViewById(R.id.route_listing_preference_recycler_view);
+ new ItemTouchHelper(new RecyclerViewCallback())
+ .attachToRecyclerView(mRouteListingPreferenceRecyclerView);
+ mRouteListingPreferenceRecyclerView.setLayoutManager(
+ new LinearLayoutManager(/* context= */ this));
+ mRouteListingPreferenceRecyclerView.setHasFixedSize(true);
+ mRouteListingPreferenceRecyclerView.setAdapter(
+ new RouteListingPreferenceRecyclerViewAdapter());
+
+ FloatingActionButton newRouteButton =
+ findViewById(R.id.new_route_listing_preference_item_button);
+ newRouteButton.setOnClickListener(
+ view ->
+ setUpRouteListingPreferenceItemEditionDialog(
+ mRoutesManager.getRouteListingPreferenceItems().size()));
+ }
+
+ private void setUpRouteListingPreferenceItemEditionDialog(int itemPositionInList) {
+ List<RouteListingPreferenceItemHolder> routeListingPreference =
+ mRoutesManager.getRouteListingPreferenceItems();
+ List<MediaRouter.RouteInfo> routesWithNoAssociatedListingPreferenceItem =
+ getRoutesWithNoAssociatedListingPreferenceItem();
+ if (itemPositionInList == routeListingPreference.size()
+ && routesWithNoAssociatedListingPreferenceItem.isEmpty()) {
+ Toast.makeText(/* context= */ this, "No (more) routes available", Toast.LENGTH_LONG)
+ .show();
+ return;
+ }
+ View dialogView =
+ getLayoutInflater()
+ .inflate(R.layout.route_listing_preference_item_dialog, /* root= */ null);
+
+ Spinner routeSpinner = dialogView.findViewById(R.id.rlp_item_dialog_route_name_spinner);
+ List<RouteListingPreferenceItemHolder> spinnerEntries = new ArrayList<>();
+
+ Spinner selectionBehaviorSpinner =
+ dialogView.findViewById(R.id.rlp_item_dialog_selection_behavior_spinner);
+ UiUtils.setUpEnumBasedSpinner(
+ /* context= */ this,
+ selectionBehaviorSpinner,
+ RouteListingPreferenceItemSelectionBehavior.SELECTION_BEHAVIOR_TRANSFER,
+ (unused) -> {});
+
+ CheckBox ongoingSessionCheckBox =
+ dialogView.findViewById(R.id.rlp_item_dialog_ongoing_session_checkbox);
+ CheckBox sessionManagedCheckBox =
+ dialogView.findViewById(R.id.rlp_item_dialog_session_managed_checkbox);
+ CheckBox suggestedRouteCheckBox =
+ dialogView.findViewById(R.id.rlp_item_dialog_suggested_checkbox);
+
+ Spinner subtextSpinner = dialogView.findViewById(R.id.rlp_item_dialog_subtext_spinner);
+ UiUtils.setUpEnumBasedSpinner(
+ /* context= */ this,
+ subtextSpinner,
+ RouteListingPreferenceItemSubtext.SUBTEXT_NONE,
+ (unused) -> {});
+
+ if (itemPositionInList < routeListingPreference.size()) {
+ RouteListingPreferenceItemHolder itemHolder =
+ routeListingPreference.get(itemPositionInList);
+ spinnerEntries.add(itemHolder);
+ int selectionBehaviorOrdinalIndex =
+ RouteListingPreferenceItemSelectionBehavior.fromConstant(
+ itemHolder.mItem.getSelectionBehavior())
+ .ordinal();
+ selectionBehaviorSpinner.setSelection(selectionBehaviorOrdinalIndex);
+ ongoingSessionCheckBox.setChecked(itemHolder.hasFlag(FLAG_ONGOING_SESSION));
+ sessionManagedCheckBox.setChecked(itemHolder.hasFlag(FLAG_ONGOING_SESSION_MANAGED));
+ suggestedRouteCheckBox.setChecked(itemHolder.hasFlag(FLAG_SUGGESTED));
+ int subtextOrdinalIndex =
+ RouteListingPreferenceItemSubtext.fromConstant(itemHolder.mItem.getSubText())
+ .ordinal();
+ subtextSpinner.setSelection(subtextOrdinalIndex);
+ }
+ for (MediaRouter.RouteInfo routeInfo : routesWithNoAssociatedListingPreferenceItem) {
+ spinnerEntries.add(
+ new RouteListingPreferenceItemHolder(
+ new RouteListingPreference.Item.Builder(routeInfo.getId()).build(),
+ routeInfo.getName()));
+ }
+ routeSpinner.setAdapter(
+ new ArrayAdapter<>(
+ /* context= */ this, android.R.layout.simple_spinner_item, spinnerEntries));
+
+ AlertDialog editRlpItemDialog =
+ new AlertDialog.Builder(this)
+ .setView(dialogView)
+ .setPositiveButton(
+ "Accept",
+ (unusedDialog, unusedWhich) -> {
+ RouteListingPreferenceItemHolder item =
+ (RouteListingPreferenceItemHolder)
+ routeSpinner.getSelectedItem();
+ RouteListingPreferenceItemSelectionBehavior selectionBehavior =
+ (RouteListingPreferenceItemSelectionBehavior)
+ selectionBehaviorSpinner.getSelectedItem();
+ int flags = 0;
+ flags |=
+ ongoingSessionCheckBox.isChecked()
+ ? FLAG_ONGOING_SESSION
+ : 0;
+ flags |=
+ sessionManagedCheckBox.isChecked()
+ ? FLAG_ONGOING_SESSION_MANAGED
+ : 0;
+ flags |=
+ suggestedRouteCheckBox.isChecked() ? FLAG_SUGGESTED : 0;
+ RouteListingPreferenceItemSubtext subtext =
+ (RouteListingPreferenceItemSubtext)
+ subtextSpinner.getSelectedItem();
+ onEditRlpItemDialogAccepted(
+ item.mItem.getRouteId(),
+ item.mRouteName,
+ selectionBehavior.mConstant,
+ flags,
+ subtext.mConstant,
+ itemPositionInList);
+ })
+ .setNegativeButton("Dismiss", (unusedDialog, unusedWhich) -> {})
+ .create();
+
+ editRlpItemDialog.show();
+ }
+
+ private void onEditRlpItemDialogAccepted(
+ String routeId,
+ String routeName,
+ int selectionBehavior,
+ int flags,
+ int subtext,
+ int itemPositionInList) {
+ ArrayList<RouteListingPreferenceItemHolder> newRouteListingPreference =
+ new ArrayList<>(mRoutesManager.getRouteListingPreferenceItems());
+ RecyclerView.Adapter<?> adapter = mRouteListingPreferenceRecyclerView.getAdapter();
+ RouteListingPreference.Item.Builder newItemBuilder =
+ new RouteListingPreference.Item.Builder(routeId)
+ .setFlags(flags)
+ .setSelectionBehavior(selectionBehavior)
+ .setSubText(subtext);
+ if (subtext == SUBTEXT_CUSTOM) {
+ newItemBuilder.setCustomSubtextMessage("A custom subtext");
+ }
+ RouteListingPreference.Item newItem = newItemBuilder.build();
+ RouteListingPreferenceItemHolder newItemAndNamePair =
+ new RouteListingPreferenceItemHolder(newItem, routeName);
+ if (itemPositionInList < newRouteListingPreference.size()) {
+ newRouteListingPreference.set(itemPositionInList, newItemAndNamePair);
+ adapter.notifyItemChanged(itemPositionInList);
+ } else {
+ newRouteListingPreference.add(newItemAndNamePair);
+ adapter.notifyItemInserted(itemPositionInList);
+ }
+ mRoutesManager.setRouteListingPreferenceItems(newRouteListingPreference);
+ }
+
+ @NonNull
+ private ImmutableList<MediaRouter.RouteInfo> getRoutesWithNoAssociatedListingPreferenceItem() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ return ImmutableList.of();
+ }
+ Set<String> routesWithAssociatedRouteListingPreferenceItem = new HashSet<>();
+ for (RouteListingPreferenceItemHolder element :
+ mRoutesManager.getRouteListingPreferenceItems()) {
+ String routeId = element.mItem.getRouteId();
+ routesWithAssociatedRouteListingPreferenceItem.add(routeId);
+ }
+
+ ImmutableList.Builder<MediaRouter.RouteInfo> resultBuilder = ImmutableList.builder();
+ for (MediaRouter.RouteInfo route : MediaRouter.getInstance(this).getRoutes()) {
+ if (!routesWithAssociatedRouteListingPreferenceItem.contains(route.getId())) {
+ resultBuilder.add(route);
+ }
+ }
+ return resultBuilder.build();
+ }
+
+ private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback {
+
+ private static final int INDEX_UNSET = -1;
+
+ private int mDraggingFromPosition;
+ private int mDraggingToPosition;
+
+ private RecyclerViewCallback() {
+ super(
+ ItemTouchHelper.UP | ItemTouchHelper.DOWN,
+ ItemTouchHelper.START | ItemTouchHelper.END);
+ mDraggingFromPosition = INDEX_UNSET;
+ mDraggingToPosition = INDEX_UNSET;
+ }
+
+ @Override
+ public boolean onMove(
+ @NonNull RecyclerView recyclerView,
+ @NonNull RecyclerView.ViewHolder origin,
+ @NonNull RecyclerView.ViewHolder target) {
+ int fromPosition = origin.getBindingAdapterPosition();
+ int toPosition = target.getBindingAdapterPosition();
+ if (mDraggingFromPosition == INDEX_UNSET) {
+ // A drag has started, but we wait for the clearView() call to update the route
+ // listing preference.
+ mDraggingFromPosition = fromPosition;
+ }
+ mDraggingToPosition = toPosition;
+ recyclerView.getAdapter().notifyItemMoved(fromPosition, toPosition);
+ return false;
+ }
+
+ @Override
+ public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
+ ArrayList<RouteListingPreferenceItemHolder> newRouteListingPreference =
+ new ArrayList<>(mRoutesManager.getRouteListingPreferenceItems());
+ int itemPosition = viewHolder.getBindingAdapterPosition();
+ newRouteListingPreference.remove(itemPosition);
+ mRoutesManager.setRouteListingPreferenceItems(newRouteListingPreference);
+ viewHolder.getBindingAdapter().notifyItemRemoved(itemPosition);
+ }
+
+ @Override
+ public void clearView(
+ @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
+ super.clearView(recyclerView, viewHolder);
+ if (mDraggingFromPosition != INDEX_UNSET) {
+ ArrayList<RouteListingPreferenceItemHolder> newRouteListingPreference =
+ new ArrayList<>(mRoutesManager.getRouteListingPreferenceItems());
+ newRouteListingPreference.add(
+ mDraggingToPosition,
+ newRouteListingPreference.remove(mDraggingFromPosition));
+ mRoutesManager.setRouteListingPreferenceItems(newRouteListingPreference);
+ }
+ mDraggingFromPosition = INDEX_UNSET;
+ mDraggingToPosition = INDEX_UNSET;
+ }
+ }
+
+ private class RouteListingPreferenceRecyclerViewAdapter
+ extends RecyclerView.Adapter<RecyclerViewItemViewHolder> {
+ @NonNull
+ @Override
+ public RecyclerViewItemViewHolder onCreateViewHolder(
+ @NonNull ViewGroup parent, int viewType) {
+ TextView textView =
+ (TextView)
+ LayoutInflater.from(parent.getContext())
+ .inflate(
+ android.R.layout.simple_list_item_1,
+ parent,
+ /* attachToRoot= */ false);
+ return new RecyclerViewItemViewHolder(textView);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerViewItemViewHolder holder, int position) {
+ holder.mTextView.setText(
+ mRoutesManager.getRouteListingPreferenceItems().get(position).mRouteName);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mRoutesManager.getRouteListingPreferenceItems().size();
+ }
+ }
+
+ private class RecyclerViewItemViewHolder extends RecyclerView.ViewHolder
+ implements View.OnClickListener {
+
+ public final TextView mTextView;
+
+ private RecyclerViewItemViewHolder(TextView textView) {
+ super(textView);
+ mTextView = textView;
+ textView.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ setUpRouteListingPreferenceItemEditionDialog(getBindingAdapterPosition());
+ }
+ }
+
+ private enum RouteListingPreferenceItemSelectionBehavior {
+ SELECTION_BEHAVIOR_NONE(RouteListingPreference.Item.SELECTION_BEHAVIOR_NONE, "None"),
+ SELECTION_BEHAVIOR_TRANSFER(
+ RouteListingPreference.Item.SELECTION_BEHAVIOR_TRANSFER, "Transfer"),
+ SELECTION_BEHAVIOR_GO_TO_APP(
+ RouteListingPreference.Item.SELECTION_BEHAVIOR_GO_TO_APP, "Go to app");
+
+ public final int mConstant;
+ public final String mHumanReadableString;
+
+ RouteListingPreferenceItemSelectionBehavior(
+ int constant, @NonNull String humanReadableString) {
+ mConstant = constant;
+ mHumanReadableString = humanReadableString;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return mHumanReadableString;
+ }
+
+ public static RouteListingPreferenceItemSelectionBehavior fromConstant(int constant) {
+ switch (constant) {
+ case RouteListingPreference.Item.SELECTION_BEHAVIOR_NONE:
+ return SELECTION_BEHAVIOR_NONE;
+ case RouteListingPreference.Item.SELECTION_BEHAVIOR_TRANSFER:
+ return SELECTION_BEHAVIOR_TRANSFER;
+ case RouteListingPreference.Item.SELECTION_BEHAVIOR_GO_TO_APP:
+ return SELECTION_BEHAVIOR_GO_TO_APP;
+ default:
+ throw new IllegalArgumentException("Illegal selection behavior: " + constant);
+ }
+ }
+ }
+
+ private enum RouteListingPreferenceItemSubtext {
+ SUBTEXT_NONE(RouteListingPreference.Item.SUBTEXT_NONE, "None"),
+ SUBTEXT_ERROR_UNKNOWN(RouteListingPreference.Item.SUBTEXT_ERROR_UNKNOWN, "Unknown error"),
+ SUBTEXT_SUBSCRIPTION_REQUIRED(
+ RouteListingPreference.Item.SUBTEXT_SUBSCRIPTION_REQUIRED, "Subscription required"),
+ SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED(
+ RouteListingPreference.Item.SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED,
+ "Downloaded content disallowed"),
+ SUBTEXT_AD_ROUTING_DISALLOWED(
+ RouteListingPreference.Item.SUBTEXT_AD_ROUTING_DISALLOWED, "Ad in progress"),
+ SUBTEXT_DEVICE_LOW_POWER(
+ RouteListingPreference.Item.SUBTEXT_DEVICE_LOW_POWER, "Device in low power mode"),
+ SUBTEXT_UNAUTHORIZED(RouteListingPreference.Item.SUBTEXT_UNAUTHORIZED, "Unauthorized"),
+ SUBTEXT_TRACK_UNSUPPORTED(
+ RouteListingPreference.Item.SUBTEXT_TRACK_UNSUPPORTED, "Track unsupported"),
+ SUBTEXT_CUSTOM(
+ RouteListingPreference.Item.SUBTEXT_CUSTOM, "Custom text (placeholder value)");
+
+ public final int mConstant;
+ @NonNull public final String mHumanReadableString;
+
+ RouteListingPreferenceItemSubtext(int constant, @NonNull String humanReadableString) {
+ mConstant = constant;
+ mHumanReadableString = humanReadableString;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return mHumanReadableString;
+ }
+
+ public static RouteListingPreferenceItemSubtext fromConstant(int constant) {
+ switch (constant) {
+ case RouteListingPreference.Item.SUBTEXT_NONE:
+ return SUBTEXT_NONE;
+ case RouteListingPreference.Item.SUBTEXT_ERROR_UNKNOWN:
+ return SUBTEXT_ERROR_UNKNOWN;
+ case RouteListingPreference.Item.SUBTEXT_SUBSCRIPTION_REQUIRED:
+ return SUBTEXT_SUBSCRIPTION_REQUIRED;
+ case RouteListingPreference.Item.SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED:
+ return SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED;
+ case RouteListingPreference.Item.SUBTEXT_AD_ROUTING_DISALLOWED:
+ return SUBTEXT_AD_ROUTING_DISALLOWED;
+ case RouteListingPreference.Item.SUBTEXT_DEVICE_LOW_POWER:
+ return SUBTEXT_DEVICE_LOW_POWER;
+ case RouteListingPreference.Item.SUBTEXT_UNAUTHORIZED:
+ return SUBTEXT_UNAUTHORIZED;
+ case RouteListingPreference.Item.SUBTEXT_TRACK_UNSUPPORTED:
+ return SUBTEXT_TRACK_UNSUPPORTED;
+ case RouteListingPreference.Item.SUBTEXT_CUSTOM:
+ return SUBTEXT_CUSTOM;
+ default:
+ throw new IllegalArgumentException("Illegal subtext constant: " + constant);
+ }
+ }
+ }
+}
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 aae3bd1..6efb10a 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
@@ -26,6 +26,7 @@
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
+import android.widget.Button;
import android.widget.Spinner;
import android.widget.Switch;
@@ -91,6 +92,13 @@
}
};
+ Button goToRouteListingPreferenceButton =
+ findViewById(R.id.go_to_route_listing_preference_button);
+ goToRouteListingPreferenceButton.setOnClickListener(
+ unusedView -> {
+ startActivity(new Intent(this, RouteListingPreferenceActivity.class));
+ });
+
RecyclerView routeList = findViewById(R.id.routes_recycler_view);
routeList.setLayoutManager(new LinearLayoutManager(/* context= */ this));
mRoutesAdapter = new RoutesAdapter(mRoutesManager.getRouteItems(), routeItemListener);
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/data/RouteItem.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/data/RouteItem.java
index eb034ed..d9e68aa 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/data/RouteItem.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/data/RouteItem.java
@@ -40,6 +40,7 @@
private int mVolumeMax;
private DeviceType mDeviceType;
private List<String> mGroupMemberIds;
+ private boolean mIsSenderDriven;
public RouteItem() {
this.mId = UUID.randomUUID().toString();
@@ -54,6 +55,7 @@
this.mDeviceType = DeviceType.UNKNOWN;
this.mCanDisconnect = false;
this.mGroupMemberIds = new ArrayList<>();
+ this.mIsSenderDriven = false;
}
public RouteItem(
@@ -68,7 +70,8 @@
int volume,
int volumeMax,
@NonNull DeviceType deviceType,
- @NonNull List<String> groupMemberIds) {
+ @NonNull List<String> groupMemberIds,
+ boolean isSenderDriven) {
mId = id;
mName = name;
mDescription = description;
@@ -81,6 +84,7 @@
mVolumeMax = volumeMax;
mDeviceType = deviceType;
mGroupMemberIds = groupMemberIds;
+ mIsSenderDriven = isSenderDriven;
}
/** Returns a deep copy of an existing {@link RouteItem}. */
@@ -98,7 +102,8 @@
routeItem.getVolume(),
routeItem.getVolumeMax(),
routeItem.getDeviceType(),
- routeItem.getGroupMemberIds());
+ routeItem.getGroupMemberIds(),
+ routeItem.isSenderDriven());
}
public enum ControlFilter {
@@ -263,4 +268,12 @@
public void setGroupMemberIds(@NonNull List<String> groupMemberIds) {
mGroupMemberIds = groupMemberIds;
}
+
+ public boolean isSenderDriven() {
+ return mIsSenderDriven;
+ }
+
+ public void setSenderDriven(boolean isSenderDriven) {
+ mIsSenderDriven = isSenderDriven;
+ }
}
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 d4b3100..3ca2448 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
@@ -224,11 +224,17 @@
mGroupDescriptor = groupRouteBuilder.build();
mTvSelectedCount = countTvFromRoute(mGroupDescriptor);
- // Initialize DynamicRouteDescriptor with all the route descriptors.
+ RoutesManager routesManager = RoutesManager.getInstance(getContext());
+
+ // Initialize DynamicRouteDescriptor with all the non-sender-driven descriptors.
List<MediaRouteDescriptor> routeDescriptors = getDescriptor().getRoutes();
if (routeDescriptors != null && !routeDescriptors.isEmpty()) {
for (MediaRouteDescriptor descriptor: routeDescriptors) {
String routeId = descriptor.getId();
+ RouteItem item = routesManager.getRouteWithId(routeId);
+ if (item != null && item.isSenderDriven()) {
+ continue;
+ }
boolean selected = memberIds.contains(routeId);
DynamicRouteDescriptor.Builder builder =
new DynamicRouteDescriptor.Builder(descriptor)
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/ui/UiUtils.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/ui/UiUtils.java
new file mode 100644
index 0000000..7d3ff97
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/ui/UiUtils.java
@@ -0,0 +1,66 @@
+/*
+ * 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 com.example.androidx.mediarouting.ui;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+
+/** Contains utility methods related to UI management. */
+public final class UiUtils {
+
+ /**
+ * Populates the given {@link Spinner} using an {@link Enum} and its possible values.
+ *
+ * @param context The context in which the spinner is to be inflated.
+ * @param spinner The {@link Spinner} to populate.
+ * @param anEnum The initially selected value.
+ * @param selectionConsumer A consumer to invoke when an element is selected.
+ */
+ public static void setUpEnumBasedSpinner(
+ @NonNull Context context,
+ @NonNull Spinner spinner,
+ @NonNull Enum<?> anEnum,
+ @NonNull Consumer<Enum<?>> selectionConsumer) {
+ Enum<?>[] enumValues = anEnum.getDeclaringClass().getEnumConstants();
+ ArrayAdapter<Enum<?>> adapter =
+ new ArrayAdapter<>(context, android.R.layout.simple_spinner_item, enumValues);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinner.setAdapter(adapter);
+ spinner.setSelection(anEnum.ordinal());
+
+ spinner.setOnItemSelectedListener(
+ new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(
+ AdapterView<?> adapterView, View view, int i, long l) {
+ selectionConsumer.accept(anEnum.getDeclaringClass().getEnumConstants()[i]);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> adapterView) {}
+ });
+ }
+
+ private UiUtils() {
+ // Prevent instantiation.
+ }
+}
diff --git a/samples/MediaRoutingDemo/src/main/res/layout/activity_add_edit_route.xml b/samples/MediaRoutingDemo/src/main/res/layout/activity_add_edit_route.xml
index 8511ab91..58e0d57 100644
--- a/samples/MediaRoutingDemo/src/main/res/layout/activity_add_edit_route.xml
+++ b/samples/MediaRoutingDemo/src/main/res/layout/activity_add_edit_route.xml
@@ -263,7 +263,7 @@
android:padding="4dp">
<Switch
- android:id="@+id/cam_disconnect_switch"
+ android:id="@+id/can_disconnect_switch"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentEnd="true"
@@ -281,6 +281,31 @@
</RelativeLayout>
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_margin="12dp"
+ android:padding="4dp">
+
+ <Switch
+ android:id="@+id/is_sender_driven_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="@string/is_sender_driven_switch_label" />
+
+ </RelativeLayout>
+
<Button
android:id="@+id/save_button"
android:layout_height="wrap_content"
diff --git a/samples/MediaRoutingDemo/src/main/res/layout/activity_route_listing_preference.xml b/samples/MediaRoutingDemo/src/main/res/layout/activity_route_listing_preference.xml
new file mode 100644
index 0000000..b932051
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/res/layout/activity_route_listing_preference.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 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">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_margin="12dp"
+ android:padding="4dp">
+
+ <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 Route Listing Preference" />
+
+ <Switch
+ android:id="@+id/enable_route_listing_preference_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true" />
+
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_margin="12dp"
+ android:padding="4dp">
+
+ <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="Prefer system ordering" />
+
+ <Switch
+ android:id="@+id/prefer_system_ordering_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true" />
+
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/route_listing_preference_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1" />
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/new_route_listing_preference_item_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_margin="20dp"
+ android:padding="0dp"
+ app:srcCompat="@drawable/ic_add" />
+
+ </RelativeLayout>
+</LinearLayout>
diff --git a/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml b/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
index 8432164..d421ef3 100644
--- a/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
+++ b/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
@@ -23,6 +23,12 @@
android:layout_height="wrap_content"
android:orientation="vertical">
+ <Button
+ android:id="@+id/go_to_route_listing_preference_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Route listing preference"/>
+
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
diff --git a/samples/MediaRoutingDemo/src/main/res/layout/route_listing_preference_item_dialog.xml b/samples/MediaRoutingDemo/src/main/res/layout/route_listing_preference_item_dialog.xml
new file mode 100644
index 0000000..3b2870b
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/res/layout/route_listing_preference_item_dialog.xml
@@ -0,0 +1,166 @@
+<?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"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_margin="12dp"
+ android:padding="4dp">
+
+ <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="Route name" />
+
+ <Spinner
+ android:id="@+id/rlp_item_dialog_route_name_spinner"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_margin="12dp"
+ android:padding="4dp">
+
+ <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="Selection behavior" />
+
+ <Spinner
+ android:id="@+id/rlp_item_dialog_selection_behavior_spinner"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true" />
+ </LinearLayout>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_margin="12dp"
+ android:padding="4dp">
+
+ <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="Ongoing session" />
+
+ <CheckBox
+ android:id="@+id/rlp_item_dialog_ongoing_session_checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true" />
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_margin="12dp"
+ android:padding="4dp">
+
+ <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="Ongoing session managed" />
+
+ <CheckBox
+ android:id="@+id/rlp_item_dialog_session_managed_checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true" />
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_margin="12dp"
+ android:padding="4dp">
+
+ <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="Suggested route" />
+
+ <CheckBox
+ android:id="@+id/rlp_item_dialog_suggested_checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true" />
+ </RelativeLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:layout_margin="12dp"
+ android:padding="4dp">
+
+ <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="Subtext" />
+
+ <Spinner
+ android:id="@+id/rlp_item_dialog_subtext_spinner"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true" />
+ </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/MediaRoutingDemo/src/main/res/values/strings.xml b/samples/MediaRoutingDemo/src/main/res/values/strings.xml
index 8cc58fa..66cff8a1 100644
--- a/samples/MediaRoutingDemo/src/main/res/values/strings.xml
+++ b/samples/MediaRoutingDemo/src/main/res/values/strings.xml
@@ -49,10 +49,14 @@
<string name="dg_not_unselectable_route_name5"> Dynamic Route 5 - Not unselectable</string>
<string name="dg_static_group_route_name6"> Dynamic Route 6 - Static Group</string>
+ <string name="sender_driven_route_name1">Sender Driven TV 1</string>
+ <string name="sender_driven_route_name2">Sender Driven TV 2</string>
+
<string name="sample_media_route_provider_remote">Remote Playback (Simulated)</string>
<string name="sample_media_route_activity_local">Local Playback</string>
<string name="sample_media_route_activity_presentation">Local Playback on Presentation Display</string>
<string name="delete_route_alert_dialog_title">Delete this route?</string>
<string name="delete_route_alert_dialog_message">Are you sure you want to delete this route?</string>
+ <string name="is_sender_driven_switch_label">Is Sender Driven</string>
</resources>
diff --git a/security/security-app-authenticator-testing/build.gradle b/security/security-app-authenticator-testing/build.gradle
index 7f3ed40..e0e8856 100644
--- a/security/security-app-authenticator-testing/build.gradle
+++ b/security/security-app-authenticator-testing/build.gradle
@@ -38,7 +38,7 @@
}
androidx {
- name = "Android Security App Package Authenticator Testing"
+ name = "Security App Authenticator Testing Extensions"
type = LibraryType.PUBLISHED_TEST_LIBRARY
mavenVersion = LibraryVersions.SECURITY_APP_AUTHENTICATOR_TESTING
inceptionYear = "2021"
diff --git a/security/security-app-authenticator/build.gradle b/security/security-app-authenticator/build.gradle
index cb80735..de6d420 100644
--- a/security/security-app-authenticator/build.gradle
+++ b/security/security-app-authenticator/build.gradle
@@ -48,7 +48,7 @@
}
androidx {
- name = "Android Security App Package Authenitcator Library"
+ name = "Security App Authenitcator"
type = LibraryType.PUBLISHED_LIBRARY
mavenVersion = LibraryVersions.SECURITY_APP_AUTHENTICATOR
inceptionYear = "2020"
diff --git a/security/security-biometric/build.gradle b/security/security-biometric/build.gradle
index c8204ad..c48792f 100644
--- a/security/security-biometric/build.gradle
+++ b/security/security-biometric/build.gradle
@@ -43,7 +43,7 @@
}
androidx {
- name = "AndroidX Security Biometric"
+ name = "Security Biometric"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.SECURITY_BIOMETRIC
inceptionYear = "2020"
diff --git a/security/security-crypto-ktx/build.gradle b/security/security-crypto-ktx/build.gradle
index adf0c8b..6717a1f 100644
--- a/security/security-crypto-ktx/build.gradle
+++ b/security/security-crypto-ktx/build.gradle
@@ -42,7 +42,7 @@
}
androidx {
- name = "AndroidX Security Kotlin Extensions"
+ name = "Security Kotlin Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.SECURITY
inceptionYear = "2020"
diff --git a/security/security-crypto/build.gradle b/security/security-crypto/build.gradle
index 4b86de9..f33cc83 100644
--- a/security/security-crypto/build.gradle
+++ b/security/security-crypto/build.gradle
@@ -46,7 +46,7 @@
}
androidx {
- name = "AndroidX Security"
+ name = "Security"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.SECURITY
inceptionYear = "2019"
diff --git a/security/security-identity-credential/build.gradle b/security/security-identity-credential/build.gradle
index d6b322a..b81043e 100644
--- a/security/security-identity-credential/build.gradle
+++ b/security/security-identity-credential/build.gradle
@@ -45,7 +45,7 @@
}
androidx {
- name = "AndroidX Security"
+ name = "Security"
publish = Publish.SNAPSHOT_AND_RELEASE
runApiTasks = new RunApiTasks.Yes("Need to track API surface before moving to publish")
mavenVersion = LibraryVersions.SECURITY_IDENTITY_CREDENTIAL
diff --git a/security/security-identity-credential/src/main/java/androidx/security/identity/HardwareIdentityCredential.java b/security/security-identity-credential/src/main/java/androidx/security/identity/HardwareIdentityCredential.java
index 855f060..096366c 100644
--- a/security/security-identity-credential/src/main/java/androidx/security/identity/HardwareIdentityCredential.java
+++ b/security/security-identity-credential/src/main/java/androidx/security/identity/HardwareIdentityCredential.java
@@ -249,6 +249,7 @@
return builder.build();
}
+ @SuppressWarnings("deprecation")
@Override
public void setAvailableAuthenticationKeys(int keyCount, int maxUsesPerKey) {
mCredential.setAvailableAuthenticationKeys(keyCount, maxUsesPerKey);
@@ -271,6 +272,7 @@
}
}
+ @SuppressWarnings("deprecation")
@Override
public @NonNull
int[] getAuthenticationDataUsageCount() {
diff --git a/settings.gradle b/settings.gradle
index 0f11a3a..a52c174 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -95,9 +95,9 @@
value("androidx.projects", getRequestedProjectSubsetName() ?: "Unset")
value("androidx.useMaxDepVersions", providers.gradleProperty("androidx.useMaxDepVersions").isPresent().toString())
- // Publish scan for androidx-main
- publishAlways()
- publishIfAuthenticated()
+ // Do not publish scan for androidx-platform-dev
+ // publishAlways()
+ // publishIfAuthenticated()
}
}
@@ -444,6 +444,7 @@
includeProject(":appsearch:appsearch-ktx", [BuildType.MAIN])
includeProject(":appsearch:appsearch-local-storage", [BuildType.MAIN])
includeProject(":appsearch:appsearch-platform-storage", [BuildType.MAIN])
+includeProject(":appsearch:appsearch-play-services-storage", [BuildType.MAIN])
includeProject(":appsearch:appsearch-test-util", [BuildType.MAIN])
includeProject(":arch:core:core-common", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE])
includeProject(":arch:core:core-runtime", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE])
@@ -683,6 +684,8 @@
includeProject(":core:core-splashscreen:core-splashscreen-samples", "core/core-splashscreen/samples", [BuildType.MAIN])
includeProject(":core:core-graphics-integration-tests:core-graphics-integration-tests", "core/core-graphics-integration-tests/testapp", [BuildType.MAIN])
includeProject(":core:core-role", [BuildType.MAIN])
+includeProject(":core:core-telecom", [BuildType.MAIN])
+includeProject(":core:core-telecom:integration-tests:testapp", [BuildType.MAIN])
includeProject(":core:uwb:uwb", [BuildType.MAIN])
includeProject(":core:uwb:uwb-rxjava3", [BuildType.MAIN])
includeProject(":credentials:credentials", [BuildType.MAIN])
@@ -757,6 +760,7 @@
includeProject(":glance:glance-wear-tiles", [BuildType.GLANCE])
includeProject(":glance:glance-wear-tiles-preview", [BuildType.GLANCE])
includeProject(":graphics:filters:filters", [BuildType.MAIN])
+includeProject(":graphics:graphics-path", [BuildType.MAIN])
includeProject(":graphics:graphics-core", [BuildType.MAIN])
includeProject(":graphics:graphics-shapes", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":graphics:integration-tests:testapp", [BuildType.MAIN])
@@ -1075,6 +1079,7 @@
includeProject(":window:window-testing", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.WINDOW])
includeProject(":work:integration-tests:testapp", [BuildType.MAIN])
includeProject(":work:work-benchmark", [BuildType.MAIN])
+includeProject(":work:work-datatransfer", [BuildType.MAIN])
includeProject(":work:work-gcm", [BuildType.MAIN])
includeProject(":work:work-inspection", [BuildType.MAIN])
includeProject(":work:work-multiprocess", [BuildType.MAIN])
diff --git a/sharetarget/sharetarget/build.gradle b/sharetarget/sharetarget/build.gradle
index 233ff32..4c1c02f 100644
--- a/sharetarget/sharetarget/build.gradle
+++ b/sharetarget/sharetarget/build.gradle
@@ -36,7 +36,7 @@
}
androidx {
- name = "AndroidX Share Target Support Library"
+ name = "Share Target"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "ShareTarget"
diff --git a/slice/slice-benchmark/build.gradle b/slice/slice-benchmark/build.gradle
index 1e151d0..6cfa1f1 100644
--- a/slice/slice-benchmark/build.gradle
+++ b/slice/slice-benchmark/build.gradle
@@ -40,7 +40,7 @@
}
androidx {
- name = "Slices Benchmarks"
+ name = "Slice Benchmarks"
publish = Publish.NONE // Library is deprecated pending removal.
mavenVersion = LibraryVersions.SLICE_BENCHMARK
inceptionYear = "2018"
diff --git a/slice/slice-builders-ktx/build.gradle b/slice/slice-builders-ktx/build.gradle
index cbd1af6..c67b6c7 100644
--- a/slice/slice-builders-ktx/build.gradle
+++ b/slice/slice-builders-ktx/build.gradle
@@ -45,7 +45,7 @@
}
androidx {
- name = "Slice builders KTX"
+ name = "Slice Builders Kotlin Extensions"
publish = Publish.SNAPSHOT_ONLY // Library is deprecated pending removal.
runApiTasks = new RunApiTasks.Yes() // Pending removal, but keep API files for now.
mavenVersion = LibraryVersions.SLICE_BUILDERS_KTX
diff --git a/slice/slice-builders/build.gradle b/slice/slice-builders/build.gradle
index a8c0dbb..06f184e 100644
--- a/slice/slice-builders/build.gradle
+++ b/slice/slice-builders/build.gradle
@@ -31,7 +31,7 @@
}
androidx {
- name = "Slice builders"
+ name = "Slice Builders"
publish = Publish.SNAPSHOT_ONLY // Library is deprecated pending removal.
runApiTasks = new RunApiTasks.Yes() // Pending removal, but keep API files for now.
mavenVersion = LibraryVersions.SLICE
diff --git a/slice/slice-core/build.gradle b/slice/slice-core/build.gradle
index 03a0441..33028b2 100644
--- a/slice/slice-core/build.gradle
+++ b/slice/slice-core/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "Common utilities for slices"
+ name = "Slice Core"
publish = Publish.SNAPSHOT_ONLY // Library is deprecated pending removal.
runApiTasks = new RunApiTasks.Yes() // Pending removal, but keep API files for now.
mavenVersion = LibraryVersions.SLICE
diff --git a/slice/slice-test/build.gradle b/slice/slice-test/build.gradle
index a7330d5..6913178 100644
--- a/slice/slice-test/build.gradle
+++ b/slice/slice-test/build.gradle
@@ -38,7 +38,7 @@
}
androidx {
- name = "Slice test code"
+ name = "Slice Test Extensions"
type = LibraryType.INTERNAL_TEST_LIBRARY
publish = Publish.NONE // Library is deprecated pending removal.
mavenVersion = LibraryVersions.SLICE
diff --git a/slice/slice-view/build.gradle b/slice/slice-view/build.gradle
index 8f8c858..14f019d 100644
--- a/slice/slice-view/build.gradle
+++ b/slice/slice-view/build.gradle
@@ -41,7 +41,7 @@
}
androidx {
- name = "Slice views"
+ name = "Slice Views"
publish = Publish.SNAPSHOT_ONLY // Library is deprecated pending removal.
runApiTasks = new RunApiTasks.Yes() // Pending removal, but keep API files for now.
mavenVersion = LibraryVersions.SLICE
diff --git a/slidingpanelayout/slidingpanelayout/api/api_lint.ignore b/slidingpanelayout/slidingpanelayout/api/api_lint.ignore
index e495753..a288bd0 100644
--- a/slidingpanelayout/slidingpanelayout/api/api_lint.ignore
+++ b/slidingpanelayout/slidingpanelayout/api/api_lint.ignore
@@ -1,10 +1,6 @@
// Baseline format: 1.0
InvalidNullabilityOverride: androidx.slidingpanelayout.widget.SlidingPaneLayout#addView(android.view.View, int, android.view.ViewGroup.LayoutParams) parameter #0:
Invalid nullability on parameter `child` in method `addView`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.slidingpanelayout.widget.SlidingPaneLayout#draw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `c` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.slidingpanelayout.widget.SlidingPaneLayout#drawChild(android.graphics.Canvas, android.view.View, long) parameter #0:
- Invalid nullability on parameter `canvas` in method `drawChild`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
InvalidNullabilityOverride: androidx.slidingpanelayout.widget.SlidingPaneLayout#removeView(android.view.View) parameter #0:
Invalid nullability on parameter `view` in method `removeView`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
@@ -15,6 +11,10 @@
MissingNullability: androidx.slidingpanelayout.widget.SlidingPaneLayout#checkLayoutParams(android.view.ViewGroup.LayoutParams) parameter #0:
Missing nullability on parameter `p` in method `checkLayoutParams`
+MissingNullability: androidx.slidingpanelayout.widget.SlidingPaneLayout#draw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `c` in method `draw`
+MissingNullability: androidx.slidingpanelayout.widget.SlidingPaneLayout#drawChild(android.graphics.Canvas, android.view.View, long) parameter #0:
+ Missing nullability on parameter `canvas` in method `drawChild`
MissingNullability: androidx.slidingpanelayout.widget.SlidingPaneLayout#drawChild(android.graphics.Canvas, android.view.View, long) parameter #1:
Missing nullability on parameter `child` in method `drawChild`
MissingNullability: androidx.slidingpanelayout.widget.SlidingPaneLayout#generateDefaultLayoutParams():
diff --git a/slidingpanelayout/slidingpanelayout/build.gradle b/slidingpanelayout/slidingpanelayout/build.gradle
index 6a29dca..58cbeb8 100644
--- a/slidingpanelayout/slidingpanelayout/build.gradle
+++ b/slidingpanelayout/slidingpanelayout/build.gradle
@@ -23,7 +23,7 @@
}
androidx {
- name = "Android Support Library Sliding Pane Layout"
+ name = "Sliding Pane Layout"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "SlidingPaneLayout offers a responsive, two pane layout that automatically switches between overlapping panes on smaller devices to a side by side view on larger devices."
diff --git a/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/helpers/TestActivity.kt b/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/helpers/TestActivity.kt
index 207861a..0e4d8ef 100644
--- a/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/helpers/TestActivity.kt
+++ b/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/helpers/TestActivity.kt
@@ -27,12 +27,14 @@
// callback when the activity created
onActivityCreated(this)
// disable enter animation
+ @Suppress("Deprecation")
overridePendingTransition(0, 0)
}
override fun finish() {
super.finish()
// disable exit animation
+ @Suppress("Deprecation")
overridePendingTransition(0, 0)
}
diff --git a/sqlite/sqlite-framework/build.gradle b/sqlite/sqlite-framework/build.gradle
index 0169f43..65eb1e4 100644
--- a/sqlite/sqlite-framework/build.gradle
+++ b/sqlite/sqlite-framework/build.gradle
@@ -37,7 +37,7 @@
}
androidx {
- name = "Android Support SQLite - Framework Implementation"
+ name = "SQLite Framework Integration"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "The implementation of Support SQLite library using the framework code."
diff --git a/sqlite/sqlite-inspection/build.gradle b/sqlite/sqlite-inspection/build.gradle
index da58266..3210e2c 100644
--- a/sqlite/sqlite-inspection/build.gradle
+++ b/sqlite/sqlite-inspection/build.gradle
@@ -38,7 +38,7 @@
}
androidx {
- name = "Android SQLite Inspector"
+ name = "SQLite Inspector"
type = LibraryType.IDE_PLUGIN
// Decouple SQLITE_INSPECTOR because it depends on
// the inspection protocol, which is alpha
diff --git a/sqlite/sqlite-ktx/build.gradle b/sqlite/sqlite-ktx/build.gradle
index a793599..5698d49 100644
--- a/sqlite/sqlite-ktx/build.gradle
+++ b/sqlite/sqlite-ktx/build.gradle
@@ -31,7 +31,7 @@
}
androidx {
- name = "Android DB KTX"
+ name = "SQLite Kotlin Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Kotlin extensions for DB"
diff --git a/sqlite/sqlite/build.gradle b/sqlite/sqlite/build.gradle
index 580912e..5d64abf 100644
--- a/sqlite/sqlite/build.gradle
+++ b/sqlite/sqlite/build.gradle
@@ -36,7 +36,7 @@
}
androidx {
- name = "Android DB"
+ name = "SQLite"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android DB"
diff --git a/startup/startup-runtime-lint/build.gradle b/startup/startup-runtime-lint/build.gradle
index 0a10bc6..2ca037c 100644
--- a/startup/startup-runtime-lint/build.gradle
+++ b/startup/startup-runtime-lint/build.gradle
@@ -32,7 +32,7 @@
}
androidx {
- name = "Android App Startup Runtime Lint Checks"
+ name = "Startup Runtime Lint Checks"
type = LibraryType.LINT
inceptionYear = "2020"
description = "Android App Startup Runtime"
diff --git a/startup/startup-runtime/build.gradle b/startup/startup-runtime/build.gradle
index 9cfaac9..d4f9766 100644
--- a/startup/startup-runtime/build.gradle
+++ b/startup/startup-runtime/build.gradle
@@ -49,7 +49,7 @@
}
androidx {
- name = "Android App Startup Runtime"
+ name = "Startup Runtime"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2020"
description = "Android App Startup Runtime"
diff --git a/swiperefreshlayout/swiperefreshlayout/api/api_lint.ignore b/swiperefreshlayout/swiperefreshlayout/api/api_lint.ignore
index 6038f08..25a9da5 100644
--- a/swiperefreshlayout/swiperefreshlayout/api/api_lint.ignore
+++ b/swiperefreshlayout/swiperefreshlayout/api/api_lint.ignore
@@ -13,6 +13,8 @@
Internal field mOriginalOffsetTop must not be exposed
+MissingNullability: androidx.swiperefreshlayout.widget.CircularProgressDrawable#draw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `draw`
MissingNullability: androidx.swiperefreshlayout.widget.CircularProgressDrawable#setColorFilter(android.graphics.ColorFilter) parameter #0:
Missing nullability on parameter `colorFilter` in method `setColorFilter`
MissingNullability: androidx.swiperefreshlayout.widget.SwipeRefreshLayout#onInterceptTouchEvent(android.view.MotionEvent) parameter #0:
diff --git a/swiperefreshlayout/swiperefreshlayout/build.gradle b/swiperefreshlayout/swiperefreshlayout/build.gradle
index 8450677..a80a5e5 100644
--- a/swiperefreshlayout/swiperefreshlayout/build.gradle
+++ b/swiperefreshlayout/swiperefreshlayout/build.gradle
@@ -30,7 +30,7 @@
}
androidx {
- name = "Android Support Library Custom View"
+ name = "Swipe Refresh Layout"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/test/ext/junit-gtest/build.gradle b/test/ext/junit-gtest/build.gradle
index be6c1fd..868ab98 100644
--- a/test/ext/junit-gtest/build.gradle
+++ b/test/ext/junit-gtest/build.gradle
@@ -34,7 +34,7 @@
}
androidx {
- name = "AndroidX JUnit GTest"
+ name = "JUnit GTest"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2022"
description = "Run GTest tests on Android devices"
diff --git a/test/screenshot/screenshot/build.gradle b/test/screenshot/screenshot/build.gradle
index 7147d25..69303c3 100644
--- a/test/screenshot/screenshot/build.gradle
+++ b/test/screenshot/screenshot/build.gradle
@@ -46,7 +46,7 @@
}
androidx {
- name = "AndroidX Library Screenshot Test"
+ name = "Screenshot Test"
type = LibraryType.INTERNAL_TEST_LIBRARY
}
diff --git a/test/uiautomator/uiautomator/build.gradle b/test/uiautomator/uiautomator/build.gradle
index ef6ca7c..b1dd210 100644
--- a/test/uiautomator/uiautomator/build.gradle
+++ b/test/uiautomator/uiautomator/build.gradle
@@ -44,7 +44,7 @@
}
androidx {
- name = "Android UIAutomator"
+ name = "UIAutomator"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2012"
description = "UI testing framework suitable for cross-app functional UI testing"
diff --git a/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/TestActivity.kt b/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/TestActivity.kt
index f44d9c4..81334f8 100644
--- a/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/TestActivity.kt
+++ b/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/TestActivity.kt
@@ -32,6 +32,7 @@
super.onCreate(savedInstanceState)
setContentView(R.layout.content_view)
println("onCreate")
+ @Suppress("Deprecation")
overridePendingTransition(0, 0)
}
@@ -48,6 +49,7 @@
}
super.finish()
+ @Suppress("Deprecation")
overridePendingTransition(0, 0)
}
diff --git a/tracing/tracing-ktx/build.gradle b/tracing/tracing-ktx/build.gradle
index 9cfe63d..a40116c 100644
--- a/tracing/tracing-ktx/build.gradle
+++ b/tracing/tracing-ktx/build.gradle
@@ -34,7 +34,7 @@
}
androidx {
- name = "Android Tracing Runtime Kotlin Extensions"
+ name = "Tracing Kotlin Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2020"
description = "Android Tracing"
diff --git a/tracing/tracing-perfetto-binary/build.gradle b/tracing/tracing-perfetto-binary/build.gradle
index 6a2f998..c663488 100644
--- a/tracing/tracing-perfetto-binary/build.gradle
+++ b/tracing/tracing-perfetto-binary/build.gradle
@@ -82,7 +82,7 @@
}
androidx {
- name = "AndroidX Tracing: Perfetto SDK Native Dependencies"
+ name = "Tracing Perfetto Binary"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.TRACING_PERFETTO
inceptionYear = "2022"
diff --git a/tracing/tracing-perfetto-common/build.gradle b/tracing/tracing-perfetto-common/build.gradle
index 4d9548c..ac22d616 100644
--- a/tracing/tracing-perfetto-common/build.gradle
+++ b/tracing/tracing-perfetto-common/build.gradle
@@ -33,7 +33,7 @@
}
androidx {
- name = "AndroidX Tracing: Perfetto Common"
+ name = "Tracing Perfetto Common"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.TRACING_PERFETTO
inceptionYear = "2022"
diff --git a/tracing/tracing-perfetto/build.gradle b/tracing/tracing-perfetto/build.gradle
index 9e6c8fb..5632eb7 100644
--- a/tracing/tracing-perfetto/build.gradle
+++ b/tracing/tracing-perfetto/build.gradle
@@ -58,7 +58,7 @@
}
androidx {
- name = "AndroidX Tracing: Perfetto SDK"
+ name = "Tracing Perfetto"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.TRACING_PERFETTO
inceptionYear = "2022"
diff --git a/tracing/tracing/build.gradle b/tracing/tracing/build.gradle
index 032edf2..1f0851b 100644
--- a/tracing/tracing/build.gradle
+++ b/tracing/tracing/build.gradle
@@ -32,7 +32,7 @@
}
androidx {
- name = "Android Tracing"
+ name = "Tracing"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2020"
description = "Android Tracing"
diff --git a/transition/transition/api/current.txt b/transition/transition/api/current.txt
index 0c3d6bf..80c0d772 100644
--- a/transition/transition/api/current.txt
+++ b/transition/transition/api/current.txt
@@ -146,6 +146,7 @@
method public String getName();
method public androidx.transition.PathMotion getPathMotion();
method public androidx.transition.TransitionPropagation? getPropagation();
+ method public final androidx.transition.Transition getRootTransition();
method public long getStartDelay();
method public java.util.List<java.lang.Integer!> getTargetIds();
method public java.util.List<java.lang.String!>? getTargetNames();
@@ -153,6 +154,7 @@
method public java.util.List<android.view.View!> getTargets();
method public String![]? getTransitionProperties();
method public androidx.transition.TransitionValues? getTransitionValues(android.view.View, boolean);
+ method public boolean isSeekingSupported();
method public boolean isTransitionRequired(androidx.transition.TransitionValues?, androidx.transition.TransitionValues?);
method public androidx.transition.Transition removeListener(androidx.transition.Transition.TransitionListener);
method public androidx.transition.Transition removeTarget(android.view.View);
@@ -180,9 +182,11 @@
public static interface Transition.TransitionListener {
method public void onTransitionCancel(androidx.transition.Transition);
method public void onTransitionEnd(androidx.transition.Transition);
+ method public default void onTransitionEnd(androidx.transition.Transition, boolean);
method public void onTransitionPause(androidx.transition.Transition);
method public void onTransitionResume(androidx.transition.Transition);
method public void onTransitionStart(androidx.transition.Transition);
+ method public default void onTransitionStart(androidx.transition.Transition, boolean);
}
public class TransitionInflater {
@@ -204,6 +208,7 @@
ctor public TransitionManager();
method public static void beginDelayedTransition(android.view.ViewGroup);
method public static void beginDelayedTransition(android.view.ViewGroup, androidx.transition.Transition?);
+ method public static androidx.transition.TransitionSeekController? controlDelayedTransition(android.view.ViewGroup, androidx.transition.Transition);
method public static void endTransitions(android.view.ViewGroup?);
method public static void go(androidx.transition.Scene);
method public static void go(androidx.transition.Scene, androidx.transition.Transition?);
@@ -219,6 +224,17 @@
method public abstract long getStartDelay(android.view.ViewGroup, androidx.transition.Transition, androidx.transition.TransitionValues?, androidx.transition.TransitionValues?);
}
+ public interface TransitionSeekController {
+ method public void addOnReadyListener(androidx.core.util.Consumer<androidx.transition.TransitionSeekController!>);
+ method public void animateToEnd();
+ method public void animateToStart();
+ method public long getCurrentPlayTimeMillis();
+ method public long getDurationMillis();
+ method public boolean isReady();
+ method public void removeOnReadyListener(androidx.core.util.Consumer<androidx.transition.TransitionSeekController!>);
+ method public void setCurrentPlayTimeMillis(long);
+ }
+
public class TransitionSet extends androidx.transition.Transition {
ctor public TransitionSet();
ctor public TransitionSet(android.content.Context, android.util.AttributeSet);
diff --git a/transition/transition/api/public_plus_experimental_current.txt b/transition/transition/api/public_plus_experimental_current.txt
index 0c3d6bf..80c0d772 100644
--- a/transition/transition/api/public_plus_experimental_current.txt
+++ b/transition/transition/api/public_plus_experimental_current.txt
@@ -146,6 +146,7 @@
method public String getName();
method public androidx.transition.PathMotion getPathMotion();
method public androidx.transition.TransitionPropagation? getPropagation();
+ method public final androidx.transition.Transition getRootTransition();
method public long getStartDelay();
method public java.util.List<java.lang.Integer!> getTargetIds();
method public java.util.List<java.lang.String!>? getTargetNames();
@@ -153,6 +154,7 @@
method public java.util.List<android.view.View!> getTargets();
method public String![]? getTransitionProperties();
method public androidx.transition.TransitionValues? getTransitionValues(android.view.View, boolean);
+ method public boolean isSeekingSupported();
method public boolean isTransitionRequired(androidx.transition.TransitionValues?, androidx.transition.TransitionValues?);
method public androidx.transition.Transition removeListener(androidx.transition.Transition.TransitionListener);
method public androidx.transition.Transition removeTarget(android.view.View);
@@ -180,9 +182,11 @@
public static interface Transition.TransitionListener {
method public void onTransitionCancel(androidx.transition.Transition);
method public void onTransitionEnd(androidx.transition.Transition);
+ method public default void onTransitionEnd(androidx.transition.Transition, boolean);
method public void onTransitionPause(androidx.transition.Transition);
method public void onTransitionResume(androidx.transition.Transition);
method public void onTransitionStart(androidx.transition.Transition);
+ method public default void onTransitionStart(androidx.transition.Transition, boolean);
}
public class TransitionInflater {
@@ -204,6 +208,7 @@
ctor public TransitionManager();
method public static void beginDelayedTransition(android.view.ViewGroup);
method public static void beginDelayedTransition(android.view.ViewGroup, androidx.transition.Transition?);
+ method public static androidx.transition.TransitionSeekController? controlDelayedTransition(android.view.ViewGroup, androidx.transition.Transition);
method public static void endTransitions(android.view.ViewGroup?);
method public static void go(androidx.transition.Scene);
method public static void go(androidx.transition.Scene, androidx.transition.Transition?);
@@ -219,6 +224,17 @@
method public abstract long getStartDelay(android.view.ViewGroup, androidx.transition.Transition, androidx.transition.TransitionValues?, androidx.transition.TransitionValues?);
}
+ public interface TransitionSeekController {
+ method public void addOnReadyListener(androidx.core.util.Consumer<androidx.transition.TransitionSeekController!>);
+ method public void animateToEnd();
+ method public void animateToStart();
+ method public long getCurrentPlayTimeMillis();
+ method public long getDurationMillis();
+ method public boolean isReady();
+ method public void removeOnReadyListener(androidx.core.util.Consumer<androidx.transition.TransitionSeekController!>);
+ method public void setCurrentPlayTimeMillis(long);
+ }
+
public class TransitionSet extends androidx.transition.Transition {
ctor public TransitionSet();
ctor public TransitionSet(android.content.Context, android.util.AttributeSet);
diff --git a/transition/transition/api/restricted_current.txt b/transition/transition/api/restricted_current.txt
index 64748fe..5ae4286 100644
--- a/transition/transition/api/restricted_current.txt
+++ b/transition/transition/api/restricted_current.txt
@@ -172,6 +172,7 @@
method public String getName();
method public androidx.transition.PathMotion getPathMotion();
method public androidx.transition.TransitionPropagation? getPropagation();
+ method public final androidx.transition.Transition getRootTransition();
method public long getStartDelay();
method public java.util.List<java.lang.Integer!> getTargetIds();
method public java.util.List<java.lang.String!>? getTargetNames();
@@ -179,6 +180,7 @@
method public java.util.List<android.view.View!> getTargets();
method public String![]? getTransitionProperties();
method public androidx.transition.TransitionValues? getTransitionValues(android.view.View, boolean);
+ method public boolean isSeekingSupported();
method public boolean isTransitionRequired(androidx.transition.TransitionValues?, androidx.transition.TransitionValues?);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void pause(android.view.View?);
method public androidx.transition.Transition removeListener(androidx.transition.Transition.TransitionListener);
@@ -213,9 +215,11 @@
public static interface Transition.TransitionListener {
method public void onTransitionCancel(androidx.transition.Transition);
method public void onTransitionEnd(androidx.transition.Transition);
+ method public default void onTransitionEnd(androidx.transition.Transition, boolean);
method public void onTransitionPause(androidx.transition.Transition);
method public void onTransitionResume(androidx.transition.Transition);
method public void onTransitionStart(androidx.transition.Transition);
+ method public default void onTransitionStart(androidx.transition.Transition, boolean);
}
public class TransitionInflater {
@@ -237,6 +241,7 @@
ctor public TransitionManager();
method public static void beginDelayedTransition(android.view.ViewGroup);
method public static void beginDelayedTransition(android.view.ViewGroup, androidx.transition.Transition?);
+ method public static androidx.transition.TransitionSeekController? controlDelayedTransition(android.view.ViewGroup, androidx.transition.Transition);
method public static void endTransitions(android.view.ViewGroup?);
method public static void go(androidx.transition.Scene);
method public static void go(androidx.transition.Scene, androidx.transition.Transition?);
@@ -252,6 +257,17 @@
method public abstract long getStartDelay(android.view.ViewGroup, androidx.transition.Transition, androidx.transition.TransitionValues?, androidx.transition.TransitionValues?);
}
+ public interface TransitionSeekController {
+ method public void addOnReadyListener(androidx.core.util.Consumer<androidx.transition.TransitionSeekController!>);
+ method public void animateToEnd();
+ method public void animateToStart();
+ method public long getCurrentPlayTimeMillis();
+ method public long getDurationMillis();
+ method public boolean isReady();
+ method public void removeOnReadyListener(androidx.core.util.Consumer<androidx.transition.TransitionSeekController!>);
+ method public void setCurrentPlayTimeMillis(long);
+ }
+
public class TransitionSet extends androidx.transition.Transition {
ctor public TransitionSet();
ctor public TransitionSet(android.content.Context, android.util.AttributeSet);
diff --git a/transition/transition/build.gradle b/transition/transition/build.gradle
index e1080ed..b899cd4 100644
--- a/transition/transition/build.gradle
+++ b/transition/transition/build.gradle
@@ -8,7 +8,7 @@
dependencies {
api("androidx.annotation:annotation:1.2.0")
- api("androidx.core:core:1.1.0")
+ api(project(":core:core"))
implementation("androidx.collection:collection:1.1.0")
compileOnly("androidx.fragment:fragment:1.2.5")
compileOnly("androidx.appcompat:appcompat:1.0.1")
@@ -22,6 +22,7 @@
androidTestImplementation(libs.espressoCore, excludes.espresso)
androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.opentest4j)
androidTestImplementation(project(":fragment:fragment"))
androidTestImplementation("androidx.appcompat:appcompat:1.1.0")
androidTestImplementation(project(":internal-testutils-runtime"), {
@@ -44,7 +45,7 @@
}
androidx {
- name = "Android Transition Support Library"
+ name = "Transition"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2016"
description = "Android Transition Support Library"
diff --git a/transition/transition/src/androidTest/java/androidx/transition/AlwaysTransition.kt b/transition/transition/src/androidTest/java/androidx/transition/AlwaysTransition.kt
new file mode 100644
index 0000000..0ac5f03
--- /dev/null
+++ b/transition/transition/src/androidTest/java/androidx/transition/AlwaysTransition.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.transition
+
+import android.animation.Animator
+import android.animation.ValueAnimator
+import android.view.ViewGroup
+
+/**
+ * A test transition that always provides an animation, regardless of start/end state
+ */
+class AlwaysTransition(private val keyPrefix: String) : Transition() {
+ override fun captureStartValues(transitionValues: TransitionValues) {
+ transitionValues.values[keyPrefix + Key] = AlwaysChangingValue++
+ }
+
+ override fun captureEndValues(transitionValues: TransitionValues) {
+ transitionValues.values[keyPrefix + Key] = AlwaysChangingValue++
+ }
+
+ override fun isSeekingSupported(): Boolean = true
+
+ override fun createAnimator(
+ sceneRoot: ViewGroup,
+ startValues: TransitionValues?,
+ endValues: TransitionValues?
+ ): Animator = ValueAnimator.ofFloat(0f, 100f)
+
+ companion object {
+ private const val Key = "alwaysChanging"
+ private var AlwaysChangingValue = 0
+ }
+}
\ No newline at end of file
diff --git a/transition/transition/src/androidTest/java/androidx/transition/BaseTransitionTest.java b/transition/transition/src/androidTest/java/androidx/transition/BaseTransitionTest.java
index a5b727f..e2ed58e 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/BaseTransitionTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/BaseTransitionTest.java
@@ -17,7 +17,7 @@
package androidx.transition;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
@@ -55,7 +55,7 @@
mRoot = (LinearLayout) rule.getActivity().findViewById(R.id.root);
mTransitionTargets.clear();
mTransition = createTransition();
- mListener = mock(Transition.TransitionListener.class);
+ mListener = spy(new TransitionListenerAdapter());
mTransition.addListener(mListener);
}
@@ -113,7 +113,7 @@
void resetListener() {
mTransition.removeListener(mListener);
- mListener = mock(Transition.TransitionListener.class);
+ mListener = spy(new TransitionListenerAdapter());
mTransition.addListener(mListener);
}
diff --git a/transition/transition/src/androidTest/java/androidx/transition/ChangeBoundsTest.java b/transition/transition/src/androidTest/java/androidx/transition/ChangeBoundsTest.java
index c1a8962..3206c710 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/ChangeBoundsTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/ChangeBoundsTest.java
@@ -19,23 +19,33 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import android.content.Context;
+import android.graphics.Rect;
import android.os.Build;
import android.view.View;
+import android.view.ViewGroup;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
+import androidx.core.os.BuildCompat;
import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.transition.test.R;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Test;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
@LargeTest
public class ChangeBoundsTest extends BaseTransitionTest {
@@ -113,7 +123,592 @@
suppressLayout.ensureExpectedValueApplied();
}
- private class TestSuppressLayout extends FrameLayout {
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void seekingChangeBoundsNoClip() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new ChangeBounds());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View[] viewArr = new View[1];
+
+ rule.runOnUiThread(() -> {
+ viewArr[0] = new View(activity);
+ mRoot.addView(viewArr[0], new ViewGroup.LayoutParams(100, 100));
+ });
+
+ final View view = viewArr[0];
+ ViewGroup parent = (ViewGroup) view.getParent();
+
+ rule.runOnUiThread(() -> {
+ assertEquals(100, view.getWidth());
+ assertEquals(100, view.getHeight());
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.width = 200;
+ layoutParams.height = 300;
+ view.setLayoutParams(layoutParams);
+ });
+ final TransitionSeekController seekController = seekControllerArr[0];
+ CountDownLatch endLatch = new CountDownLatch(1);
+
+ rule.runOnUiThread(() -> {
+ assertEquals(100, view.getWidth());
+ assertEquals(100, view.getHeight());
+ assertTrue(parent.isLayoutSuppressed());
+
+ // Seek past the always there transition before the change bounds
+ seekController.setCurrentPlayTimeMillis(300);
+ assertEquals(100, view.getWidth());
+ assertEquals(100, view.getHeight());
+ assertTrue(parent.isLayoutSuppressed());
+
+ // Seek to half through the change bounds
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(150, view.getWidth());
+ assertEquals(200, view.getHeight());
+ assertTrue(parent.isLayoutSuppressed());
+
+ // Seek past the ChangeBounds
+ seekController.setCurrentPlayTimeMillis(800);
+ assertEquals(200, view.getWidth());
+ assertEquals(300, view.getHeight());
+ assertTrue(parent.isLayoutSuppressed());
+
+ // Seek back to half through the change bounds
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(150, view.getWidth());
+ assertEquals(200, view.getHeight());
+ assertTrue(parent.isLayoutSuppressed());
+
+ // Seek before the change bounds:
+ seekController.setCurrentPlayTimeMillis(250);
+ assertEquals(100, view.getWidth());
+ assertEquals(100, view.getHeight());
+ assertTrue(parent.isLayoutSuppressed());
+
+ seekController.setCurrentPlayTimeMillis(450);
+ ChangeBounds returnTransition = new ChangeBounds();
+ returnTransition.addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ endLatch.countDown();
+ }
+ });
+ TransitionManager.beginDelayedTransition(mRoot, returnTransition);
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.width = 100;
+ layoutParams.height = 100;
+ view.setLayoutParams(layoutParams);
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should start from 150x200 through and head toward 100x100
+ assertTrue(150 >= view.getWidth());
+ assertTrue(200 >= view.getHeight());
+ });
+
+ assertTrue(endLatch.await(3, TimeUnit.SECONDS));
+ rule.runOnUiThread(() -> {
+ assertFalse(parent.isLayoutSuppressed());
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void seekingChangeBoundsWithClip() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ ChangeBounds changeBounds = new ChangeBounds();
+ changeBounds.setResizeClip(true);
+ transition.addTransition(changeBounds);
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View[] viewArr = new View[1];
+
+ rule.runOnUiThread(() -> {
+ viewArr[0] = new View(activity);
+ mRoot.addView(viewArr[0], new ViewGroup.LayoutParams(400, 300));
+ });
+
+ final View view = viewArr[0];
+ ViewGroup parent = (ViewGroup) view.getParent();
+
+ rule.runOnUiThread(() -> {
+ assertEquals(400, view.getWidth());
+ assertEquals(300, view.getHeight());
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.width = 300;
+ layoutParams.height = 200;
+ view.setLayoutParams(layoutParams);
+ });
+ final TransitionSeekController seekController = seekControllerArr[0];
+ CountDownLatch endLatch = new CountDownLatch(1);
+
+ rule.runOnUiThread(() -> {
+ assertEquals(400, view.getWidth());
+ assertEquals(300, view.getHeight());
+ assertEquals(new Rect(0, 0, 400, 300), view.getClipBounds());
+ assertTrue(parent.isLayoutSuppressed());
+
+ // Seek past the always there transition before the change bounds
+ seekController.setCurrentPlayTimeMillis(300);
+ assertEquals(400, view.getWidth());
+ assertEquals(300, view.getHeight());
+ assertEquals(new Rect(0, 0, 400, 300), view.getClipBounds());
+ assertTrue(parent.isLayoutSuppressed());
+
+ // Seek to half through the change bounds
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(400, view.getWidth());
+ assertEquals(300, view.getHeight());
+ assertEquals(new Rect(0, 0, 350, 250), view.getClipBounds());
+ assertTrue(parent.isLayoutSuppressed());
+
+ // Seek past the ChangeBounds
+ seekController.setCurrentPlayTimeMillis(800);
+ assertEquals(300, view.getWidth());
+ assertEquals(200, view.getHeight());
+ assertNull(view.getClipBounds());
+ assertTrue(parent.isLayoutSuppressed());
+
+ // Seek back to half through the change bounds
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(400, view.getWidth());
+ assertEquals(300, view.getHeight());
+ assertEquals(new Rect(0, 0, 350, 250), view.getClipBounds());
+ assertTrue(parent.isLayoutSuppressed());
+
+ // Seek before the change bounds:
+ seekController.setCurrentPlayTimeMillis(250);
+ assertEquals(400, view.getWidth());
+ assertEquals(300, view.getHeight());
+ assertNull(view.getClipBounds());
+ assertTrue(parent.isLayoutSuppressed());
+
+ seekController.setCurrentPlayTimeMillis(450);
+ ChangeBounds returnTransition = new ChangeBounds();
+ returnTransition.setResizeClip(true);
+ returnTransition.addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ endLatch.countDown();
+ }
+ });
+ TransitionManager.beginDelayedTransition(mRoot, returnTransition);
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.width = 200;
+ layoutParams.height = 500;
+ view.setLayoutParams(layoutParams);
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should start from 400x500, clipped to 350x250
+ assertEquals(400, view.getWidth());
+ assertEquals(500, view.getHeight());
+ assertTrue(view.getClipBounds().width() <= 350);
+ assertTrue(view.getClipBounds().height() >= 250);
+ });
+
+ assertTrue(endLatch.await(3, TimeUnit.SECONDS));
+ rule.runOnUiThread(() -> {
+ assertEquals(200, view.getWidth());
+ assertEquals(500, view.getHeight());
+ assertFalse(parent.isLayoutSuppressed());
+ assertNull(view.getClipBounds());
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void interruptedBeforeStartNoClip() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ ChangeBounds changeBounds = new ChangeBounds();
+ transition.addTransition(changeBounds);
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View[] viewArr = new View[1];
+
+ rule.runOnUiThread(() -> {
+ viewArr[0] = new View(activity);
+ mRoot.addView(viewArr[0], new ViewGroup.LayoutParams(400, 300));
+ });
+
+ final View view = viewArr[0];
+
+ rule.runOnUiThread(() -> {
+ TransitionManager.controlDelayedTransition(mRoot, transition);
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.width = 300;
+ layoutParams.height = 500;
+ view.setLayoutParams(layoutParams);
+ });
+
+ rule.runOnUiThread(() -> {
+ ChangeBounds change = new ChangeBounds();
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, change);
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.width = 100;
+ layoutParams.height = 150;
+ view.setLayoutParams(layoutParams);
+ });
+
+ final TransitionSeekController seekController = seekControllerArr[0];
+
+ rule.runOnUiThread(() -> {
+ // It should start from 400x300
+ assertEquals(400, view.getWidth());
+ assertEquals(300, view.getHeight());
+
+ // go halfway through
+ seekController.setCurrentPlayTimeMillis(150);
+ assertEquals(250, view.getWidth());
+ assertEquals(225, view.getHeight());
+
+ // skip to the end
+ seekController.setCurrentPlayTimeMillis(300);
+ assertEquals(100, view.getWidth());
+ assertEquals(150, view.getHeight());
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void interruptedBeforeStartWithClip() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ ChangeBounds changeBounds = new ChangeBounds();
+ changeBounds.setResizeClip(true);
+ transition.addTransition(changeBounds);
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View[] viewArr = new View[1];
+
+ rule.runOnUiThread(() -> {
+ viewArr[0] = new View(activity);
+ mRoot.addView(viewArr[0], new ViewGroup.LayoutParams(400, 300));
+ });
+
+ final View view = viewArr[0];
+
+ rule.runOnUiThread(() -> {
+ TransitionManager.controlDelayedTransition(mRoot, transition);
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.width = 300;
+ layoutParams.height = 500;
+ view.setLayoutParams(layoutParams);
+ });
+
+ rule.runOnUiThread(() -> {
+ ChangeBounds change = new ChangeBounds();
+ change.setResizeClip(true);
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, change);
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.width = 600;
+ layoutParams.height = 150;
+ view.setLayoutParams(layoutParams);
+ });
+
+ final TransitionSeekController seekController = seekControllerArr[0];
+
+ rule.runOnUiThread(() -> {
+ // It should start from 600x500, clipped to 400x300
+ assertEquals(600, view.getWidth());
+ assertEquals(500, view.getHeight());
+ assertEquals(400, view.getClipBounds().width());
+ assertEquals(300, view.getClipBounds().height());
+
+ // go halfway through
+ seekController.setCurrentPlayTimeMillis(150);
+ assertEquals(500, view.getClipBounds().width());
+ assertEquals(225, view.getClipBounds().height());
+
+ // skip to the end
+ seekController.setCurrentPlayTimeMillis(300);
+ assertEquals(600, view.getWidth());
+ assertEquals(150, view.getHeight());
+ assertNull(view.getClipBounds());
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void interruptedAfterEndNoClip() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ ChangeBounds changeBounds = new ChangeBounds();
+ transition.addTransition(changeBounds);
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View[] viewArr = new View[1];
+
+ rule.runOnUiThread(() -> {
+ viewArr[0] = new View(activity);
+ mRoot.addView(viewArr[0], new ViewGroup.LayoutParams(400, 300));
+ });
+
+ final View view = viewArr[0];
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ TransitionManager.controlDelayedTransition(mRoot, transition);
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.width = 300;
+ layoutParams.height = 500;
+ view.setLayoutParams(layoutParams);
+ });
+ TransitionSeekController seekController1 = seekControllerArr[0];
+
+ rule.runOnUiThread(() -> {
+ seekController1.setCurrentPlayTimeMillis(800);
+ ChangeBounds change = new ChangeBounds();
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, change);
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.width = 100;
+ layoutParams.height = 150;
+ view.setLayoutParams(layoutParams);
+ });
+
+ final TransitionSeekController seekController2 = seekControllerArr[0];
+
+ rule.runOnUiThread(() -> {
+ // It should start from 300x500
+ assertEquals(300, view.getWidth());
+ assertEquals(500, view.getHeight());
+
+ // go halfway through
+ seekController2.setCurrentPlayTimeMillis(150);
+ assertEquals(200, view.getWidth());
+ assertEquals(325, view.getHeight());
+
+ // skip to the end
+ seekController2.setCurrentPlayTimeMillis(300);
+ assertEquals(100, view.getWidth());
+ assertEquals(150, view.getHeight());
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void interruptedAfterEndWithClip() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ ChangeBounds changeBounds = new ChangeBounds();
+ changeBounds.setResizeClip(true);
+ transition.addTransition(changeBounds);
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View[] viewArr = new View[1];
+
+ rule.runOnUiThread(() -> {
+ viewArr[0] = new View(activity);
+ mRoot.addView(viewArr[0], new ViewGroup.LayoutParams(400, 300));
+ });
+
+ final View view = viewArr[0];
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ TransitionManager.controlDelayedTransition(mRoot, transition);
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.width = 300;
+ layoutParams.height = 500;
+ view.setLayoutParams(layoutParams);
+ });
+ TransitionSeekController seekController1 = seekControllerArr[0];
+
+ rule.runOnUiThread(() -> {
+ seekController1.setCurrentPlayTimeMillis(800);
+ ChangeBounds change = new ChangeBounds();
+ change.setResizeClip(true);
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, change);
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.width = 100;
+ layoutParams.height = 150;
+ view.setLayoutParams(layoutParams);
+ });
+
+ final TransitionSeekController seekController2 = seekControllerArr[0];
+
+ rule.runOnUiThread(() -> {
+ // It should start from 300x500, clipped to 300x500
+ assertEquals(300, view.getWidth());
+ assertEquals(500, view.getHeight());
+ assertEquals(300, view.getClipBounds().width());
+ assertEquals(500, view.getClipBounds().height());
+
+ // go halfway through
+ seekController2.setCurrentPlayTimeMillis(150);
+ assertEquals(200, view.getClipBounds().width());
+ assertEquals(325, view.getClipBounds().height());
+
+ // skip to the end
+ seekController2.setCurrentPlayTimeMillis(300);
+ assertEquals(100, view.getWidth());
+ assertEquals(150, view.getHeight());
+ assertNull(view.getClipBounds());
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void startTransitionAfterSeeking() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ ChangeBounds changeBounds = new ChangeBounds();
+ changeBounds.setResizeClip(true);
+ transition.addTransition(changeBounds);
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View[] viewArr = new View[1];
+
+ rule.runOnUiThread(() -> {
+ viewArr[0] = new View(activity);
+ mRoot.addView(viewArr[0], new ViewGroup.LayoutParams(400, 300));
+ });
+
+ final View view = viewArr[0];
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ TransitionManager.controlDelayedTransition(mRoot, transition);
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.width = 300;
+ layoutParams.height = 500;
+ view.setLayoutParams(layoutParams);
+ });
+ TransitionSeekController seekController1 = seekControllerArr[0];
+
+ rule.runOnUiThread(() -> {
+ seekController1.setCurrentPlayTimeMillis(900);
+ seekController1.setCurrentPlayTimeMillis(0);
+
+ ChangeBounds change = new ChangeBounds();
+ change.setResizeClip(true);
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, change);
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.width = 100;
+ layoutParams.height = 150;
+ view.setLayoutParams(layoutParams);
+ });
+
+ rule.runOnUiThread(() -> {
+ assertEquals(400, view.getClipBounds().width());
+ assertEquals(300, view.getClipBounds().height());
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void seekNoChange() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new ChangeBounds());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View[] viewArr = new View[1];
+
+ rule.runOnUiThread(() -> {
+ viewArr[0] = new View(activity);
+ mRoot.addView(viewArr[0], new ViewGroup.LayoutParams(400, 300));
+ });
+
+ final View view = viewArr[0];
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ TransitionManager.controlDelayedTransition(mRoot, transition);
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.width = 300;
+ layoutParams.height = 500;
+ view.setLayoutParams(layoutParams);
+ });
+ TransitionSeekController seekController1 = seekControllerArr[0];
+
+ rule.runOnUiThread(() -> {
+ seekController1.setCurrentPlayTimeMillis(450);
+ seekControllerArr[0] =
+ TransitionManager.controlDelayedTransition(mRoot, new ChangeBounds());
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.width = 300;
+ layoutParams.height = 500;
+ view.setLayoutParams(layoutParams);
+ });
+
+ rule.runOnUiThread(() -> {
+ assertEquals(0, seekControllerArr[0].getDurationMillis());
+ assertEquals(350, view.getWidth());
+ assertEquals(400, view.getHeight());
+
+ // First seek controls the transition
+ seekController1.setCurrentPlayTimeMillis(900);
+ assertEquals(300, view.getWidth());
+ assertEquals(500, view.getHeight());
+ });
+ }
+
+ private static class TestSuppressLayout extends FrameLayout {
private boolean mExpectedSuppressLayout;
private Boolean mActualSuppressLayout;
diff --git a/transition/transition/src/androidTest/java/androidx/transition/ChangeClipBoundsTest.java b/transition/transition/src/androidTest/java/androidx/transition/ChangeClipBoundsTest.java
index fc3808d5..f255f62 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/ChangeClipBoundsTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/ChangeClipBoundsTest.java
@@ -19,6 +19,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
@@ -27,8 +28,11 @@
import android.graphics.Color;
import android.graphics.Rect;
+import android.os.Build;
import android.view.View;
+import android.view.ViewGroup;
+import androidx.core.os.BuildCompat;
import androidx.core.view.ViewCompat;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
@@ -36,6 +40,9 @@
import org.junit.Test;
import org.mockito.ArgumentMatcher;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
@LargeTest
public class ChangeClipBoundsTest extends BaseTransitionTest {
@@ -103,6 +110,407 @@
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void seekingClipToNull() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new ChangeClipBounds());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View[] viewArr = new View[1];
+
+ rule.runOnUiThread(() -> {
+ viewArr[0] = new View(activity);
+ viewArr[0].setClipBounds(new Rect(0, 0, 50, 50));
+ mRoot.addView(viewArr[0], new ViewGroup.LayoutParams(100, 100));
+ });
+ final View view = viewArr[0];
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ view.setClipBounds(null);
+ });
+
+ final TransitionSeekController seekController = seekControllerArr[0];
+ ChangeClipBounds returnTransition = new ChangeClipBounds();
+ CountDownLatch latch = new CountDownLatch(1);
+ returnTransition.addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ latch.countDown();
+ }
+ });
+
+ rule.runOnUiThread(() -> {
+ assertEquals(new Rect(0, 0, 50, 50), view.getClipBounds());
+
+ // Seek past the always there transition before the clip transition
+ seekController.setCurrentPlayTimeMillis(300);
+ assertEquals(new Rect(0, 0, 50, 50), view.getClipBounds());
+
+ // Seek to half through the transition
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(new Rect(0, 0, 75, 75), view.getClipBounds());
+
+ // Seek past the transition
+ seekController.setCurrentPlayTimeMillis(800);
+ assertNull(view.getClipBounds());
+
+ // Seek back to half through the transition
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(new Rect(0, 0, 75, 75), view.getClipBounds());
+
+ // Seek before the transition:
+ seekController.setCurrentPlayTimeMillis(250);
+ assertEquals(new Rect(0, 0, 50, 50), view.getClipBounds());
+
+ seekController.setCurrentPlayTimeMillis(450);
+ TransitionManager.beginDelayedTransition(mRoot, returnTransition);
+ view.setClipBounds(new Rect(0, 0, 50, 50));
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should start from 75x75 and then transition in
+ assertTrue(view.getClipBounds().width() <= 75);
+ assertTrue(view.getClipBounds().height() <= 75);
+ });
+
+ assertTrue(latch.await(3, TimeUnit.SECONDS));
+ rule.runOnUiThread(() -> {
+ assertEquals(new Rect(0, 0, 50, 50), view.getClipBounds());
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void seekingClipFromNull() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new ChangeClipBounds());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View[] viewArr = new View[1];
+
+ rule.runOnUiThread(() -> {
+ viewArr[0] = new View(activity);
+ mRoot.addView(viewArr[0], new ViewGroup.LayoutParams(100, 100));
+ });
+ final View view = viewArr[0];
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ view.setClipBounds(new Rect(0, 0, 50, 50));
+ });
+
+ final TransitionSeekController seekController = seekControllerArr[0];
+ ChangeClipBounds returnTransition = new ChangeClipBounds();
+ CountDownLatch latch = new CountDownLatch(1);
+ returnTransition.addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ latch.countDown();
+ }
+ });
+
+ rule.runOnUiThread(() -> {
+ assertNull(view.getClipBounds());
+
+ // Seek past the always there transition before the clip transition
+ seekController.setCurrentPlayTimeMillis(300);
+ assertEquals(new Rect(0, 0, 100, 100), view.getClipBounds());
+
+ // Seek to half through the transition
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(new Rect(0, 0, 75, 75), view.getClipBounds());
+
+ // Seek past the transition
+ seekController.setCurrentPlayTimeMillis(800);
+ assertEquals(new Rect(0, 0, 50, 50), view.getClipBounds());
+
+ // Seek back to half through the transition
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(new Rect(0, 0, 75, 75), view.getClipBounds());
+
+ // Seek before the transition:
+ seekController.setCurrentPlayTimeMillis(250);
+ assertNull(view.getClipBounds());
+
+ seekController.setCurrentPlayTimeMillis(450);
+ TransitionManager.beginDelayedTransition(mRoot, returnTransition);
+ view.setClipBounds(null);
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should start from 75x75 and then transition in
+ assertTrue(view.getClipBounds().width() >= 75);
+ assertTrue(view.getClipBounds().height() >= 75);
+ });
+
+ assertTrue(latch.await(3, TimeUnit.SECONDS));
+ rule.runOnUiThread(() -> {
+ assertNull(view.getClipBounds());
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void seekingClips() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new ChangeClipBounds());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View[] viewArr = new View[1];
+
+ rule.runOnUiThread(() -> {
+ viewArr[0] = new View(activity);
+ mRoot.addView(viewArr[0], new ViewGroup.LayoutParams(100, 100));
+ viewArr[0].setClipBounds(new Rect(0, 0, 50, 50));
+ });
+ final View view = viewArr[0];
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ view.setClipBounds(new Rect(0, 0, 80, 80));
+ });
+
+ final TransitionSeekController seekController = seekControllerArr[0];
+ ChangeClipBounds returnTransition = new ChangeClipBounds();
+ CountDownLatch latch = new CountDownLatch(1);
+ returnTransition.addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ latch.countDown();
+ }
+ });
+
+ rule.runOnUiThread(() -> {
+ assertEquals(new Rect(0, 0, 50, 50), view.getClipBounds());
+
+ // Seek past the always there transition before the clip transition
+ seekController.setCurrentPlayTimeMillis(300);
+ assertEquals(new Rect(0, 0, 50, 50), view.getClipBounds());
+
+ // Seek to half through the transition
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(new Rect(0, 0, 65, 65), view.getClipBounds());
+
+ // Seek past the transition
+ seekController.setCurrentPlayTimeMillis(800);
+ assertEquals(new Rect(0, 0, 80, 80), view.getClipBounds());
+
+ // Seek back to half through the transition
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(new Rect(0, 0, 65, 65), view.getClipBounds());
+
+ // Seek before the transition:
+ seekController.setCurrentPlayTimeMillis(250);
+ assertEquals(new Rect(0, 0, 50, 50), view.getClipBounds());
+
+ seekController.setCurrentPlayTimeMillis(450);
+ TransitionManager.beginDelayedTransition(mRoot, returnTransition);
+ view.setClipBounds(new Rect(0, 0, 50, 50));
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should start from 75x75 and then transition in
+ assertTrue(view.getClipBounds().width() <= 65);
+ assertTrue(view.getClipBounds().height() <= 65);
+ });
+
+ assertTrue(latch.await(3, TimeUnit.SECONDS));
+ rule.runOnUiThread(() -> {
+ assertEquals(new Rect(0, 0, 50, 50), view.getClipBounds());
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void changeClipBeforeStart() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new ChangeClipBounds());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View[] viewArr = new View[1];
+
+ rule.runOnUiThread(() -> {
+ viewArr[0] = new View(activity);
+ mRoot.addView(viewArr[0], new ViewGroup.LayoutParams(100, 100));
+ viewArr[0].setClipBounds(new Rect(0, 0, 20, 50));
+ });
+ final View view = viewArr[0];
+ rule.runOnUiThread(() -> {
+ TransitionManager.controlDelayedTransition(mRoot, transition);
+ view.setClipBounds(new Rect(0, 0, 80, 80));
+ });
+
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] =
+ TransitionManager.controlDelayedTransition(mRoot, new ChangeClipBounds());
+ view.setClipBounds(new Rect(0, 0, 100, 100));
+ });
+
+ rule.runOnUiThread(() -> {
+ TransitionSeekController seekController = seekControllerArr[0];
+ // It should start from 20x50 and go to 100x100
+ assertEquals(20, view.getClipBounds().width());
+ assertEquals(50, view.getClipBounds().height());
+
+ // half way through
+ seekController.setCurrentPlayTimeMillis(150);
+ assertEquals(60, view.getClipBounds().width());
+ assertEquals(75, view.getClipBounds().height());
+
+ // finish
+ seekController.setCurrentPlayTimeMillis(300);
+ assertEquals(100, view.getClipBounds().width());
+ assertEquals(100, view.getClipBounds().height());
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void testSeekInterruption() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // supported on U+
+ }
+ final View view = new View(rule.getActivity());
+
+ rule.runOnUiThread(() -> {
+ mRoot.addView(view, new ViewGroup.LayoutParams(100, 100));
+ });
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new ChangeClipBounds());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ view.setClipBounds(new Rect(0, 0, 50, 50));
+ });
+
+ rule.runOnUiThread(() -> {
+ assertNull(view.getClipBounds());
+
+ // Seek to the end
+ seekControllerArr[0].setCurrentPlayTimeMillis(900);
+ assertEquals(new Rect(0, 0, 50, 50), view.getClipBounds());
+
+ // Seek back to the beginning
+ seekControllerArr[0].setCurrentPlayTimeMillis(0);
+ assertNull(view.getClipBounds());
+
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ view.setClipBounds(new Rect(50, 50, 100, 100));
+ });
+
+ rule.runOnUiThread(() -> {
+ assertNull(view.getClipBounds());
+
+ // Seek to the end
+ seekControllerArr[0].setCurrentPlayTimeMillis(900);
+ assertEquals(new Rect(50, 50, 100, 100), view.getClipBounds());
+
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ view.setClipBounds(null);
+ });
+
+ rule.runOnUiThread(() -> {
+ assertEquals(new Rect(50, 50, 100, 100), view.getClipBounds());
+
+ // Seek to the end
+ seekControllerArr[0].setCurrentPlayTimeMillis(900);
+ assertNull(view.getClipBounds());
+
+ // Seek to the middle
+ seekControllerArr[0].setCurrentPlayTimeMillis(450);
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ view.setClipBounds(new Rect(0, 0, 50, 50));
+ });
+
+ rule.runOnUiThread(() -> {
+ assertEquals(new Rect(25, 25, 100, 100), view.getClipBounds());
+
+ // Seek to the end
+ seekControllerArr[0].setCurrentPlayTimeMillis(900);
+ assertEquals(new Rect(0, 0, 50, 50), view.getClipBounds());
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void testSeekNoChange() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // supported on U+
+ }
+ final View view = new View(rule.getActivity());
+
+ rule.runOnUiThread(() -> {
+ mRoot.addView(view, new ViewGroup.LayoutParams(100, 100));
+ });
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new ChangeClipBounds());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ view.setClipBounds(new Rect(0, 0, 50, 50));
+ });
+
+ TransitionSeekController firstController = seekControllerArr[0];
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] =
+ TransitionManager.controlDelayedTransition(mRoot, new ChangeClipBounds());
+ view.setClipBounds(new Rect(0, 0, 50, 50));
+ });
+
+ rule.runOnUiThread(() -> {
+ assertEquals(0, seekControllerArr[0].getDurationMillis());
+
+ // Should only be controlled by the first transition
+ firstController.setCurrentPlayTimeMillis(900);
+ assertEquals(new Rect(0, 0, 50, 50), view.getClipBounds());
+ });
+ }
+
private ArgumentMatcher<Rect> isRectContaining(final Rect rect) {
return new ArgumentMatcher<Rect>() {
@Override
diff --git a/transition/transition/src/androidTest/java/androidx/transition/ChangeImageTransformTest.java b/transition/transition/src/androidTest/java/androidx/transition/ChangeImageTransformTest.java
index 8ddefd7..ebff1c4 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/ChangeImageTransformTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/ChangeImageTransformTest.java
@@ -40,7 +40,9 @@
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
+import androidx.core.os.BuildCompat;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.transition.test.R;
@@ -153,6 +155,88 @@
assertEquals(ImageView.ScaleType.CENTER_CROP, imageView.getScaleType());
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void testSeekInterruption() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final ImageView imageView = enterImageViewScene(ImageView.ScaleType.FIT_START,
+ null, false);
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new ChangeImageTransform());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ imageView.setScaleType(ImageView.ScaleType.FIT_XY);
+ });
+
+ rule.runOnUiThread(() -> {
+ verifyMatrixMatches(fitStartMatrix(), getDrawMatrixCompat(imageView));
+ // Seek to the end
+ seekControllerArr[0].setCurrentPlayTimeMillis(900);
+ verifyMatrixMatches(fitXYMatrix(), getDrawMatrixCompat(imageView));
+ // Seek back to the beginning
+ seekControllerArr[0].setCurrentPlayTimeMillis(0);
+ verifyMatrixMatches(fitStartMatrix(), getDrawMatrixCompat(imageView));
+
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ imageView.setScaleType(ImageView.ScaleType.FIT_END);
+ });
+
+ rule.runOnUiThread(() -> {
+ verifyMatrixMatches(fitStartMatrix(), getDrawMatrixCompat(imageView));
+ // Seek to the end
+ seekControllerArr[0].setCurrentPlayTimeMillis(900);
+ verifyMatrixMatches(fitEndMatrix(), getDrawMatrixCompat(imageView));
+
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ imageView.setScaleType(ImageView.ScaleType.FIT_START);
+ });
+
+ rule.runOnUiThread(() -> {
+ verifyMatrixMatches(fitEndMatrix(), getDrawMatrixCompat(imageView));
+ // Seek to the end
+ seekControllerArr[0].setCurrentPlayTimeMillis(900);
+ verifyMatrixMatches(fitStartMatrix(), getDrawMatrixCompat(imageView));
+
+ // Seek to the middle
+ seekControllerArr[0].setCurrentPlayTimeMillis(450);
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ imageView.setScaleType(ImageView.ScaleType.FIT_XY);
+ });
+
+ rule.runOnUiThread(() -> {
+ verifyMatrixMatches(betweenStartAndEnd(), getDrawMatrixCompat(imageView));
+ // Seek to the end
+ seekControllerArr[0].setCurrentPlayTimeMillis(900);
+ verifyMatrixMatches(fitXYMatrix(), getDrawMatrixCompat(imageView));
+ });
+ }
+
+ private Matrix betweenStartAndEnd() {
+ Matrix start = fitStartMatrix();
+ float[] startVals = new float[9];
+ start.getValues(startVals);
+ Matrix end = fitEndMatrix();
+ float[] endVals = new float[9];
+ end.getValues(endVals);
+
+ float[] middleVals = new float[9];
+ for (int i = 0; i < 9; i++) {
+ middleVals[i] = (startVals[i] + endVals[i]) / 2f;
+ }
+ Matrix middle = new Matrix();
+ middle.setValues(middleVals);
+ return middle;
+ }
+
private Matrix centerMatrix() {
int imageWidth = mImage.getIntrinsicWidth();
int imageViewWidth = mImageView.getWidth();
diff --git a/transition/transition/src/androidTest/java/androidx/transition/ChangeScrollTest.java b/transition/transition/src/androidTest/java/androidx/transition/ChangeScrollTest.java
index 3f94b5a..68414e5 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/ChangeScrollTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/ChangeScrollTest.java
@@ -19,15 +19,21 @@
import static androidx.transition.AtLeastOnceWithin.atLeastOnceWithin;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalMatchers.and;
import static org.mockito.AdditionalMatchers.gt;
import static org.mockito.AdditionalMatchers.leq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import android.os.Build;
+import android.view.View;
+import android.view.ViewGroup;
import android.widget.TextView;
+import androidx.core.os.BuildCompat;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.transition.test.R;
@@ -76,4 +82,193 @@
});
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void seekingScroll() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new ChangeScroll());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View[] viewArr = new View[1];
+
+ rule.runOnUiThread(() -> {
+ viewArr[0] = new View(activity);
+ mRoot.addView(viewArr[0], new ViewGroup.LayoutParams(100, 100));
+ });
+
+ final View view = viewArr[0];
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ view.setScrollY(100);
+ });
+
+ final TransitionSeekController seekController = seekControllerArr[0];
+
+ rule.runOnUiThread(() -> {
+ assertEquals(0, view.getScrollY());
+
+ // Seek past the always there transition before the scroll
+ seekController.setCurrentPlayTimeMillis(300);
+ assertEquals(0, view.getScrollY());
+
+ // Seek to half through the scroll
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(50, view.getScrollY());
+
+ // Seek past the scroll
+ seekController.setCurrentPlayTimeMillis(800);
+ assertEquals(100, view.getScrollY());
+
+ // Seek back to half through the scroll
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(50, view.getScrollY());
+
+ // Seek before the scroll:
+ seekController.setCurrentPlayTimeMillis(250);
+ assertEquals(0, view.getScrollY());
+
+ seekController.setCurrentPlayTimeMillis(450);
+ TransitionManager.beginDelayedTransition(mRoot, new ChangeScroll());
+ view.setScrollY(0);
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should start from 50 and move to 0
+ assertTrue(view.getScrollY() <= 50);
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void seekingScrollBeforeStart() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new ChangeScroll());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View[] viewArr = new View[1];
+
+ rule.runOnUiThread(() -> {
+ viewArr[0] = new View(activity);
+ mRoot.addView(viewArr[0], new ViewGroup.LayoutParams(100, 100));
+ viewArr[0].setScrollY(100);
+ });
+
+ final View view = viewArr[0];
+
+ rule.runOnUiThread(() -> {
+ TransitionManager.controlDelayedTransition(mRoot, transition);
+ view.setScrollY(200);
+ });
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] =
+ TransitionManager.controlDelayedTransition(mRoot, new ChangeScroll());
+ view.setScrollY(0);
+ });
+
+ final TransitionSeekController seekController = seekControllerArr[0];
+
+ rule.runOnUiThread(() -> {
+ // Should start from 100 and go to 0
+ assertEquals(100, view.getScrollY());
+
+ // Seek to half through the scroll
+ seekController.setCurrentPlayTimeMillis(150);
+ assertEquals(50, view.getScrollY());
+
+ // Seek to the end
+ seekController.setCurrentPlayTimeMillis(300);
+ assertEquals(0, view.getScrollY());
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void testSeekInterruption() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final View view = new View(rule.getActivity());
+
+ rule.runOnUiThread(() -> {
+ mRoot.addView(view, new ViewGroup.LayoutParams(100, 100));
+ });
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new ChangeScroll());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ view.setScrollY(100);
+ });
+
+ rule.runOnUiThread(() -> {
+ assertEquals(0, view.getScrollY());
+
+ // Seek to the end
+ seekControllerArr[0].setCurrentPlayTimeMillis(900);
+ assertEquals(100, view.getScrollY());
+
+ // Seek back to the beginning
+ seekControllerArr[0].setCurrentPlayTimeMillis(0);
+ assertEquals(0, view.getScrollY());
+
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ view.setScrollY(200);
+ });
+
+ rule.runOnUiThread(() -> {
+ assertEquals(0, view.getScrollY());
+
+ // Seek to the end
+ seekControllerArr[0].setCurrentPlayTimeMillis(900);
+ assertEquals(200, view.getScrollY());
+
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ view.setScrollY(50);
+ });
+
+ rule.runOnUiThread(() -> {
+ assertEquals(200, view.getScrollY());
+
+ // Seek to the end
+ seekControllerArr[0].setCurrentPlayTimeMillis(900);
+ assertEquals(50, view.getScrollY());
+
+ // Seek to the middle
+ seekControllerArr[0].setCurrentPlayTimeMillis(450);
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ view.setScrollY(500);
+ });
+
+ rule.runOnUiThread(() -> {
+ assertEquals((200 + 50) / 2, view.getScrollY());
+
+ // Seek to the end
+ seekControllerArr[0].setCurrentPlayTimeMillis(900);
+ assertEquals(500, view.getScrollY());
+ });
+ }
}
diff --git a/transition/transition/src/androidTest/java/androidx/transition/ExplodeTest.java b/transition/transition/src/androidTest/java/androidx/transition/ExplodeTest.java
index 34954a9..7a9fc71 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/ExplodeTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/ExplodeTest.java
@@ -20,15 +20,22 @@
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.graphics.Color;
+import android.os.Build;
import android.view.Gravity;
import android.view.View;
+import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
+import androidx.core.os.BuildCompat;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import org.hamcrest.Description;
@@ -176,6 +183,279 @@
assertThat(Arrays.asList(1f, 9f, 3f), is(not(decreasing())));
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void seekingExplode() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new Explode());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ this.mRedSquare.setVisibility(View.GONE);
+ });
+
+ final TransitionSeekController seekController = seekControllerArr[0];
+
+ float[] translationValues = new float[2];
+
+ rule.runOnUiThread(() -> {
+ assertEquals(1f, ViewUtils.getTransitionAlpha(mRedSquare), 0f);
+ assertEquals(View.VISIBLE, mRedSquare.getVisibility());
+ assertEquals(0f, mRedSquare.getTranslationX(), 0f);
+ assertEquals(0f, mRedSquare.getTranslationY(), 0f);
+
+ // Seek past the always there transition before the explode
+ seekController.setCurrentPlayTimeMillis(300);
+ assertEquals(View.VISIBLE, mRedSquare.getVisibility());
+ assertEquals(0f, mRedSquare.getTranslationX(), 0f);
+ assertEquals(0f, mRedSquare.getTranslationY(), 0f);
+
+ // Seek half way:
+ seekController.setCurrentPlayTimeMillis(450);
+ assertNotEquals(0f, mRedSquare.getTranslationX(), 0.01f);
+ assertNotEquals(0f, mRedSquare.getTranslationY(), 0.01f);
+ assertEquals(View.VISIBLE, mRedSquare.getVisibility());
+ translationValues[0] = mRedSquare.getTranslationX();
+ translationValues[1] = mRedSquare.getTranslationY();
+
+ // Seek past the end
+ seekController.setCurrentPlayTimeMillis(800);
+ assertEquals(0f, mRedSquare.getTranslationX(), 0f);
+ assertEquals(0f, mRedSquare.getTranslationY(), 0f);
+ assertEquals(View.GONE, mRedSquare.getVisibility());
+
+ // Seek before the explode:
+ seekController.setCurrentPlayTimeMillis(250);
+ assertEquals(View.VISIBLE, mRedSquare.getVisibility());
+ assertEquals(0f, mRedSquare.getTranslationX(), 0f);
+ assertEquals(0f, mRedSquare.getTranslationY(), 0f);
+
+ seekController.setCurrentPlayTimeMillis(450);
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, new Explode());
+ mRedSquare.setVisibility(View.VISIBLE);
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should start from half way values and decrease
+ assertEquals(translationValues[0], mRedSquare.getTranslationX(), 1f);
+ assertEquals(translationValues[1], mRedSquare.getTranslationY(), 1f);
+ assertEquals(View.VISIBLE, mRedSquare.getVisibility());
+ seekControllerArr[0].setCurrentPlayTimeMillis(300);
+
+ assertEquals(View.VISIBLE, mRedSquare.getVisibility());
+ assertEquals(0f, mRedSquare.getTranslationX(), 0f);
+ assertEquals(0f, mRedSquare.getTranslationY(), 0f);
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void seekingImplode() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ Explode implode = new Explode();
+ implode.setInterpolator(new LinearInterpolator());
+ transition.addTransition(implode);
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ rule.runOnUiThread(() -> {
+ mRedSquare.setVisibility(View.GONE);
+ });
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ mRedSquare.setVisibility(View.VISIBLE);
+ });
+
+ final TransitionSeekController seekController = seekControllerArr[0];
+
+ float[] translationValues = new float[2];
+
+ rule.runOnUiThread(() -> {
+ assertEquals(1f, ViewUtils.getTransitionAlpha(mRedSquare), 0f);
+ assertEquals(View.VISIBLE, mRedSquare.getVisibility());
+ assertNotEquals(0f, mRedSquare.getTranslationX(), 0.01f);
+ assertNotEquals(0f, mRedSquare.getTranslationY(), 0.01f);
+
+ float startX = mRedSquare.getTranslationX();
+ float startY = mRedSquare.getTranslationY();
+
+ // Seek past the always there transition before the explode
+ seekController.setCurrentPlayTimeMillis(300);
+ assertEquals(View.VISIBLE, mRedSquare.getVisibility());
+ assertEquals(startX, mRedSquare.getTranslationX(), 0f);
+ assertEquals(startY, mRedSquare.getTranslationY(), 0f);
+
+ // Seek half way:
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(startX / 2f, mRedSquare.getTranslationX(), 0.01f);
+ assertEquals(startY / 2f, mRedSquare.getTranslationY(), 0.01f);
+ assertEquals(View.VISIBLE, mRedSquare.getVisibility());
+ translationValues[0] = mRedSquare.getTranslationX();
+ translationValues[1] = mRedSquare.getTranslationY();
+
+ // Seek past the end
+ seekController.setCurrentPlayTimeMillis(800);
+ assertEquals(0f, mRedSquare.getTranslationX(), 0f);
+ assertEquals(0f, mRedSquare.getTranslationY(), 0f);
+ assertEquals(View.VISIBLE, mRedSquare.getVisibility());
+
+ // Seek before the explode:
+ seekController.setCurrentPlayTimeMillis(250);
+ assertEquals(View.VISIBLE, mRedSquare.getVisibility());
+ assertEquals(startX, mRedSquare.getTranslationX(), 0f);
+ assertEquals(startY, mRedSquare.getTranslationY(), 0f);
+
+ seekController.setCurrentPlayTimeMillis(450);
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, new Explode());
+ mRedSquare.setVisibility(View.GONE);
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should start from half way values and increase
+ assertEquals(translationValues[0], mRedSquare.getTranslationX(), 1f);
+ assertEquals(translationValues[1], mRedSquare.getTranslationY(), 1f);
+ assertEquals(View.VISIBLE, mRedSquare.getVisibility());
+
+ seekControllerArr[0].setCurrentPlayTimeMillis(300);
+ assertEquals(View.GONE, mRedSquare.getVisibility());
+ assertEquals(0f, mRedSquare.getTranslationX(), 0f);
+ assertEquals(0f, mRedSquare.getTranslationY(), 0f);
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void seekingImplodeBeforeStart() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ Explode implode = new Explode();
+ implode.setInterpolator(new LinearInterpolator());
+ transition.addTransition(implode);
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ rule.runOnUiThread(() -> {
+ mRedSquare.setVisibility(View.GONE);
+ });
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ mRedSquare.setVisibility(View.VISIBLE);
+ });
+
+ float[] translationValues = new float[2];
+
+ rule.runOnUiThread(() -> {
+ translationValues[0] = mRedSquare.getTranslationX();
+ translationValues[1] = mRedSquare.getTranslationY();
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, new Explode());
+ mRedSquare.setVisibility(View.GONE);
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should start from all the way out
+ assertEquals(translationValues[0], mRedSquare.getTranslationX(), 1f);
+ assertEquals(translationValues[1], mRedSquare.getTranslationY(), 1f);
+ assertEquals(View.VISIBLE, mRedSquare.getVisibility());
+
+ seekControllerArr[0].setCurrentPlayTimeMillis(300);
+ assertEquals(View.GONE, mRedSquare.getVisibility());
+ assertEquals(0f, mRedSquare.getTranslationX(), 0f);
+ assertEquals(0f, mRedSquare.getTranslationY(), 0f);
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void seekWithTranslation() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ rule.runOnUiThread(() -> {
+ mRedSquare.setTranslationX(1f);
+ mRedSquare.setTranslationY(5f);
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, new Explode());
+ mRedSquare.setVisibility(View.GONE);
+ });
+
+ final float[] interruptedTranslation = new float[2];
+
+ rule.runOnUiThread(() -> {
+ assertEquals(1f, mRedSquare.getTranslationX(), 0.01f);
+ assertEquals(5f, mRedSquare.getTranslationY(), 0.01f);
+
+
+ seekControllerArr[0].setCurrentPlayTimeMillis(150);
+ interruptedTranslation[0] = mRedSquare.getTranslationX();
+ interruptedTranslation[1] = mRedSquare.getTranslationY();
+
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, new Explode());
+ mRedSquare.setVisibility(View.VISIBLE);
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should start from half way values and increase
+ assertEquals(interruptedTranslation[0], mRedSquare.getTranslationX(), 1f);
+ assertEquals(interruptedTranslation[1], mRedSquare.getTranslationY(), 1f);
+
+ // make sure it would go to the start value
+ seekControllerArr[0].setCurrentPlayTimeMillis(300);
+ assertEquals(1f, mRedSquare.getTranslationX(), 0.01f);
+ assertEquals(5f, mRedSquare.getTranslationY(), 0.01f);
+
+ // Now go back to the interrupted position again:
+ seekControllerArr[0].setCurrentPlayTimeMillis(0);
+ assertEquals(interruptedTranslation[0], mRedSquare.getTranslationX(), 1f);
+ assertEquals(interruptedTranslation[1], mRedSquare.getTranslationY(), 1f);
+
+ // Send it back to GONE
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, new Explode());
+ mRedSquare.setVisibility(View.GONE);
+ });
+
+ rule.runOnUiThread(() -> {
+ assertEquals(interruptedTranslation[0], mRedSquare.getTranslationX(), 1f);
+ assertEquals(interruptedTranslation[1], mRedSquare.getTranslationY(), 1f);
+
+ // it should move away (toward the top-left)
+ seekControllerArr[0].setCurrentPlayTimeMillis(299);
+ assertTrue(mRedSquare.getTranslationX() < interruptedTranslation[0]);
+ assertTrue(mRedSquare.getTranslationY() < interruptedTranslation[1]);
+
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, new Explode());
+ mRedSquare.setVisibility(View.VISIBLE);
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should end up at the initial translation
+ seekControllerArr[0].setCurrentPlayTimeMillis(300);
+ assertEquals(1f, mRedSquare.getTranslationX(), 0.01f);
+ assertEquals(5f, mRedSquare.getTranslationY(), 0.01f);
+ });
+ }
+
private Matcher<View> hasVisibility(final int visibility) {
return new TypeSafeMatcher<View>() {
@Override
diff --git a/transition/transition/src/androidTest/java/androidx/transition/FadeTest.java b/transition/transition/src/androidTest/java/androidx/transition/FadeTest.java
index 16a9bbe..a55c8c5 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/FadeTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/FadeTest.java
@@ -25,9 +25,11 @@
import static org.hamcrest.Matchers.lessThan;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
@@ -40,8 +42,10 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.os.BuildCompat;
import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.testutils.AnimationDurationScaleRule;
import androidx.transition.test.R;
@@ -126,7 +130,7 @@
float[] valuesOut = new float[2];
final InterruptibleFade fadeOut = new InterruptibleFade(Fade.MODE_OUT, interrupt,
valuesOut);
- final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class);
+ final Transition.TransitionListener listenerOut = spy(new TransitionListenerAdapter());
fadeOut.addListener(listenerOut);
changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE);
verify(listenerOut, timeout(3000)).onTransitionStart(any(Transition.class));
@@ -137,7 +141,7 @@
// Fade in
float[] valuesIn = new float[2];
final InterruptibleFade fadeIn = new InterruptibleFade(Fade.MODE_IN, null, valuesIn);
- final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class);
+ final Transition.TransitionListener listenerIn = spy(new TransitionListenerAdapter());
fadeIn.addListener(listenerIn);
changeVisibility(fadeIn, mRoot, mView, View.VISIBLE);
verify(listenerOut, timeout(3000)).onTransitionPause(any(Transition.class));
@@ -162,7 +166,7 @@
final Runnable interrupt = mock(Runnable.class);
float[] valuesIn = new float[2];
final InterruptibleFade fadeIn = new InterruptibleFade(Fade.MODE_IN, interrupt, valuesIn);
- final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class);
+ final Transition.TransitionListener listenerIn = spy(new TransitionListenerAdapter());
fadeIn.addListener(listenerIn);
changeVisibility(fadeIn, mRoot, mView, View.VISIBLE);
verify(listenerIn, timeout(3000)).onTransitionStart(any(Transition.class));
@@ -173,7 +177,7 @@
// Fade out
float[] valuesOut = new float[2];
final InterruptibleFade fadeOut = new InterruptibleFade(Fade.MODE_OUT, null, valuesOut);
- final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class);
+ final Transition.TransitionListener listenerOut = spy(new TransitionListenerAdapter());
fadeOut.addListener(listenerOut);
changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE);
verify(listenerIn, timeout(3000)).onTransitionPause(any(Transition.class));
@@ -199,14 +203,14 @@
});
// Fade out
final Fade fadeOut = new Fade(Fade.OUT);
- final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class);
+ final Transition.TransitionListener listenerOut = spy(new TransitionListenerAdapter());
fadeOut.addListener(listenerOut);
changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE);
verify(listenerOut, timeout(3000)).onTransitionStart(any(Transition.class));
verify(listenerOut, timeout(3000)).onTransitionEnd(any(Transition.class));
// Fade in
final Fade fadeIn = new Fade(Fade.IN);
- final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class);
+ final Transition.TransitionListener listenerIn = spy(new TransitionListenerAdapter());
fadeIn.addListener(listenerIn);
changeVisibility(fadeIn, mRoot, mView, View.VISIBLE);
verify(listenerIn, timeout(3000)).onTransitionStart(any(Transition.class));
@@ -237,7 +241,7 @@
// We don't really care how short the duration is, so let's make it really short
final Fade fade = new Fade();
fade.setDuration(1);
- Transition.TransitionListener listener = mock(Transition.TransitionListener.class);
+ Transition.TransitionListener listener = spy(new TransitionListenerAdapter());
fade.addListener(listener);
rule.runOnUiThread(new Runnable() {
@@ -262,6 +266,194 @@
assertNotNull(activity.findViewById(R.id.redSquare));
}
+ @Test
+ public void seekingFadeIn() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new Fade());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View[] viewArr = new View[1];
+
+ rule.runOnUiThread(() -> {
+ viewArr[0] = new View(activity);
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ mRoot.addView(viewArr[0], new ViewGroup.LayoutParams(100, 100));
+ });
+
+ final View view = viewArr[0];
+ final TransitionSeekController seekController = seekControllerArr[0];
+
+ rule.runOnUiThread(() -> {
+ assertEquals(0f, ViewUtils.getTransitionAlpha(view), 0f);
+ assertEquals(View.VISIBLE, view.getVisibility());
+ assertEquals(View.LAYER_TYPE_NONE, view.getLayerType());
+
+ // Seek past the always there transition before the fade
+ seekController.setCurrentPlayTimeMillis(300);
+ assertEquals(0f, ViewUtils.getTransitionAlpha(view), 0f);
+ assertEquals(View.LAYER_TYPE_HARDWARE, view.getLayerType());
+
+ // Seek to half through the fade
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(0.5f, ViewUtils.getTransitionAlpha(view), 0.01f);
+
+ // Seek past the fade
+ seekController.setCurrentPlayTimeMillis(800);
+ assertEquals(View.LAYER_TYPE_NONE, view.getLayerType());
+ assertEquals(1f, ViewUtils.getTransitionAlpha(view), 0f);
+
+ // Seek back to half through the fade
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(View.LAYER_TYPE_HARDWARE, view.getLayerType());
+ assertEquals(0.5f, ViewUtils.getTransitionAlpha(view), 0f);
+
+ // Seek before the fade:
+ seekController.setCurrentPlayTimeMillis(250);
+ assertEquals(0f, ViewUtils.getTransitionAlpha(view), 0f);
+ assertEquals(View.VISIBLE, view.getVisibility());
+ assertEquals(View.LAYER_TYPE_NONE, view.getLayerType());
+
+ seekController.setCurrentPlayTimeMillis(450);
+ TransitionManager.beginDelayedTransition(mRoot, new Fade());
+ view.setVisibility(View.INVISIBLE);
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should start from 0.5 and then fade out
+ assertTrue(ViewUtils.getTransitionAlpha(view) <= 0.5f);
+ });
+ }
+
+ @Test
+ public void seekingFadeOut() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new Fade());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View[] viewArr = new View[1];
+
+ rule.runOnUiThread(() -> {
+ viewArr[0] = new View(activity);
+ mRoot.addView(viewArr[0], new ViewGroup.LayoutParams(100, 100));
+ });
+
+ final View view = viewArr[0];
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ view.setVisibility(View.GONE);
+ });
+
+ final TransitionSeekController seekController = seekControllerArr[0];
+
+ rule.runOnUiThread(() -> {
+ assertEquals(1f, ViewUtils.getTransitionAlpha(view), 0f);
+ assertEquals(View.VISIBLE, view.getVisibility());
+ assertEquals(View.LAYER_TYPE_NONE, view.getLayerType());
+
+ // Seek past the always there transition before the fade
+ seekController.setCurrentPlayTimeMillis(300);
+ assertEquals(View.VISIBLE, view.getVisibility());
+ assertEquals(1f, ViewUtils.getTransitionAlpha(view), 0f);
+ assertEquals(View.LAYER_TYPE_HARDWARE, view.getLayerType());
+
+ // Seek to half through the fade
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(0.5f, ViewUtils.getTransitionAlpha(view), 0.01f);
+
+ // Seek past the fade
+ seekController.setCurrentPlayTimeMillis(800);
+ assertEquals(View.LAYER_TYPE_NONE, view.getLayerType());
+ assertEquals(1f, ViewUtils.getTransitionAlpha(view), 0f);
+ assertEquals(View.GONE, view.getVisibility());
+
+ // Seek back to half through the fade
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(View.VISIBLE, view.getVisibility());
+ assertEquals(View.LAYER_TYPE_HARDWARE, view.getLayerType());
+ assertEquals(0.5f, ViewUtils.getTransitionAlpha(view), 0f);
+
+ // Seek before the fade:
+ seekController.setCurrentPlayTimeMillis(250);
+ assertEquals(1f, ViewUtils.getTransitionAlpha(view), 0f);
+ assertEquals(View.VISIBLE, view.getVisibility());
+ assertEquals(View.LAYER_TYPE_NONE, view.getLayerType());
+
+ seekController.setCurrentPlayTimeMillis(450);
+ TransitionManager.beginDelayedTransition(mRoot, transition);
+ view.setVisibility(View.VISIBLE);
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should start from 0.5 and then fade in
+ assertTrue(ViewUtils.getTransitionAlpha(view) >= 0.5f);
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void seekingFadeInBeforeStart() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new Fade());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View view = new View(rule.getActivity());
+
+ // Animate it in
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ mRoot.addView(view, new ViewGroup.LayoutParams(100, 100));
+ });
+
+ rule.runOnUiThread(() -> {
+ // Starts off invisible
+ assertEquals(0f, view.getTransitionAlpha(), 0f);
+ assertEquals(View.VISIBLE, view.getVisibility());
+
+ // Fade all the way in
+ seekControllerArr[0].setCurrentPlayTimeMillis(600);
+ assertEquals(1f, view.getTransitionAlpha(), 0f);
+ assertEquals(View.VISIBLE, view.getVisibility());
+
+ // Fade out again
+ seekControllerArr[0].setCurrentPlayTimeMillis(0);
+ assertEquals(0f, view.getTransitionAlpha(), 0f);
+
+ // Animate to GONE
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, new Fade());
+ view.setVisibility(View.GONE);
+ });
+
+ rule.runOnUiThread(() -> {
+ // It is already not shown, so it can just go straight to GONE
+ assertEquals(1f, view.getTransitionAlpha(), 0f);
+ assertEquals(View.GONE, view.getVisibility());
+ });
+ }
+
private void changeVisibility(final Fade fade, final ViewGroup container, final View target,
final int visibility) throws Throwable {
rule.runOnUiThread(new Runnable() {
diff --git a/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSupportTest.java b/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSupportTest.java
index 70e3f16..91efb1c 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSupportTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSupportTest.java
@@ -19,7 +19,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
@@ -213,8 +213,8 @@
}
private Transition createTransition(int type) {
- final Transition.TransitionListener listener = mock(
- Transition.TransitionListener.class);
+ final Transition.TransitionListener listener = spy(
+ new TransitionListenerAdapter());
final AutoTransition transition = new AutoTransition();
transition.addListener(listener);
transition.setDuration(10);
diff --git a/transition/transition/src/androidTest/java/androidx/transition/MultipleRootsTest.java b/transition/transition/src/androidTest/java/androidx/transition/MultipleRootsTest.java
index 131351b..57ea293 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/MultipleRootsTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/MultipleRootsTest.java
@@ -19,7 +19,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
@@ -98,9 +98,9 @@
final ActivityScenario<TransitionActivity> scenario = prepareScenario(views);
final Transition.TransitionListener innerListener =
- mock(Transition.TransitionListener.class);
+ spy(new TransitionListenerAdapter());
final Transition.TransitionListener outerListener =
- mock(Transition.TransitionListener.class);
+ spy(new TransitionListenerAdapter());
final Fade innerTransition = new Fade();
innerTransition.setDuration(300);
@@ -143,9 +143,9 @@
final ActivityScenario<TransitionActivity> scenario = prepareScenario(views);
final Transition.TransitionListener innerListener =
- mock(Transition.TransitionListener.class);
+ spy(new TransitionListenerAdapter());
final Transition.TransitionListener outerListener =
- mock(Transition.TransitionListener.class);
+ spy(new TransitionListenerAdapter());
final Fade outerTransition = new Fade();
outerTransition.setDuration(300);
@@ -188,9 +188,9 @@
final ActivityScenario<TransitionActivity> scenario = prepareScenario(views);
final Transition.TransitionListener row1Listener =
- mock(Transition.TransitionListener.class);
+ spy(new TransitionListenerAdapter());
final Transition.TransitionListener row2Listener =
- mock(Transition.TransitionListener.class);
+ spy(new TransitionListenerAdapter());
final Fade row1Transition = new Fade();
row1Transition.setDuration(300);
@@ -234,7 +234,7 @@
// For Row 1, we run a subsequent transition at the end of the first transition.
final Transition.TransitionListener row1SecondListener =
- mock(Transition.TransitionListener.class);
+ spy(new TransitionListenerAdapter());
final Fade row1SecondTransition = new Fade();
row1SecondTransition.setDuration(250);
row1SecondTransition.addListener(row1SecondListener);
@@ -253,7 +253,7 @@
// Only one transition for row 2.
final Transition.TransitionListener row2Listener =
- mock(Transition.TransitionListener.class);
+ spy(new TransitionListenerAdapter());
final Slide row2Transition = new Slide();
row2Transition.setDuration(300);
row2Transition.addListener(row2Listener);
diff --git a/transition/transition/src/androidTest/java/androidx/transition/SeekTransitionTest.kt b/transition/transition/src/androidTest/java/androidx/transition/SeekTransitionTest.kt
new file mode 100644
index 0000000..36f41b2
--- /dev/null
+++ b/transition/transition/src/androidTest/java/androidx/transition/SeekTransitionTest.kt
@@ -0,0 +1,946 @@
+/*
+ * 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.transition
+
+import android.graphics.Color
+import android.os.Build
+import android.view.View
+import android.view.ViewGroup
+import android.view.animation.LinearInterpolator
+import android.widget.LinearLayout
+import androidx.core.os.BuildCompat
+import androidx.core.util.Consumer
+import androidx.test.annotation.UiThreadTest
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.testutils.AnimationDurationScaleRule.Companion.createForAllTests
+import androidx.transition.Transition.TransitionListener
+import androidx.transition.test.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.any
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+@MediumTest
+class SeekTransitionTest : BaseTest() {
+ @get:Rule
+ val animationDurationScaleRule = createForAllTests(1f)
+
+ lateinit var view: View
+ lateinit var root: LinearLayout
+ lateinit var transition: Transition
+
+ @UiThreadTest
+ @Before
+ fun setUp() {
+ InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
+ root = rule.activity.findViewById<View>(R.id.root) as LinearLayout
+ transition = Fade().also {
+ it.interpolator = LinearInterpolator()
+ }
+ view = View(root.context)
+ view.setBackgroundColor(Color.BLUE)
+ root.addView(view, ViewGroup.LayoutParams(100, 100))
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ @UiThreadTest
+ fun onlySeekingTransitions() {
+ if (!BuildCompat.isAtLeastU()) throw IllegalArgumentException()
+ transition = object : Visibility() {}
+ TransitionManager.controlDelayedTransition(root, transition)
+ fail("Expected IllegalArgumentException")
+ }
+
+ @Test
+ fun waitForReady() {
+ if (!BuildCompat.isAtLeastU()) return
+ lateinit var seekController: TransitionSeekController
+
+ @Suppress("UNCHECKED_CAST")
+ val readyCall: Consumer<TransitionSeekController> =
+ mock(Consumer::class.java) as Consumer<TransitionSeekController>
+
+ rule.runOnUiThread {
+ val controller = TransitionManager.controlDelayedTransition(root, transition)
+ assertThat(controller).isNotNull()
+ seekController = controller!!
+ assertThat(seekController.isReady).isFalse()
+ seekController.addOnReadyListener(readyCall)
+ view.visibility = View.GONE
+ }
+
+ verify(readyCall, timeout(3000)).accept(seekController)
+ assertThat(seekController.isReady).isTrue()
+ }
+
+ @Test
+ fun waitForReadyNoChange() {
+ if (!BuildCompat.isAtLeastU()) return
+ lateinit var seekController: TransitionSeekController
+
+ @Suppress("UNCHECKED_CAST")
+ val readyCall: Consumer<TransitionSeekController> =
+ mock(Consumer::class.java) as Consumer<TransitionSeekController>
+
+ rule.runOnUiThread {
+ val controller = TransitionManager.controlDelayedTransition(root, transition)
+ assertThat(controller).isNotNull()
+ seekController = controller!!
+ assertThat(seekController.isReady).isFalse()
+ seekController.addOnReadyListener(readyCall)
+ view.requestLayout()
+ }
+
+ verify(readyCall, timeout(3000)).accept(seekController)
+ }
+
+ @Test
+ fun addListenerAfterReady() {
+ if (!BuildCompat.isAtLeastU()) return
+ lateinit var seekController: TransitionSeekController
+
+ @Suppress("UNCHECKED_CAST")
+ val readyCall: Consumer<TransitionSeekController> =
+ mock(Consumer::class.java) as Consumer<TransitionSeekController>
+
+ rule.runOnUiThread {
+ val controller = TransitionManager.controlDelayedTransition(root, transition)
+ assertThat(controller).isNotNull()
+ seekController = controller!!
+ assertThat(seekController.isReady).isFalse()
+ seekController.addOnReadyListener(readyCall)
+ view.visibility = View.GONE
+ }
+
+ verify(readyCall, timeout(3000)).accept(seekController)
+
+ @Suppress("UNCHECKED_CAST")
+ val readyCall2: Consumer<TransitionSeekController> =
+ mock(Consumer::class.java) as Consumer<TransitionSeekController>
+
+ seekController.addOnReadyListener(readyCall2)
+ verify(readyCall, times(1)).accept(seekController)
+ }
+
+ @Test
+ fun seekTransition() {
+ if (!BuildCompat.isAtLeastU()) return
+ lateinit var seekController: TransitionSeekController
+
+ val listener = spy(TransitionListenerAdapter())
+ transition.addListener(listener)
+
+ rule.runOnUiThread {
+ val controller = TransitionManager.controlDelayedTransition(root, transition)
+ assertThat(controller).isNotNull()
+ seekController = controller!!
+ assertThat(seekController.isReady).isFalse()
+ view.visibility = View.GONE
+ }
+
+ verify(listener, timeout(1000)).onTransitionStart(any())
+ verify(listener, times(0)).onTransitionEnd(any())
+
+ rule.runOnUiThread {
+ assertThat(view.visibility).isEqualTo(View.VISIBLE)
+
+ assertThat(seekController.durationMillis).isEqualTo(300)
+ assertThat(seekController.currentPlayTimeMillis).isEqualTo(0)
+
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+
+ seekController.currentPlayTimeMillis = 150
+ assertThat(view.transitionAlpha).isEqualTo(0.5f)
+ seekController.currentPlayTimeMillis = 299
+ assertThat(view.transitionAlpha).isWithin(0.001f).of(1f / 300f)
+ seekController.currentPlayTimeMillis = 300
+
+ verify(listener, times(1)).onTransitionEnd(any())
+
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+ assertThat(view.visibility).isEqualTo(View.GONE)
+ }
+ }
+
+ @Test
+ fun animationDoesNotTakeOverSeek() {
+ if (!BuildCompat.isAtLeastU()) return
+ lateinit var seekController: TransitionSeekController
+
+ val stateListener1 = spy(TransitionListenerAdapter())
+ transition.addListener(stateListener1)
+ rule.runOnUiThread {
+ val controller = TransitionManager.controlDelayedTransition(root, transition)
+ assertThat(controller).isNotNull()
+ seekController = controller!!
+ assertThat(seekController.isReady).isFalse()
+ view.visibility = View.GONE
+ }
+
+ verify(stateListener1, timeout(3000)).onTransitionStart(any())
+
+ val stateListener2 = spy(TransitionListenerAdapter())
+ val transition2 = Fade()
+ transition2.addListener(stateListener2)
+
+ rule.runOnUiThread {
+ seekController.currentPlayTimeMillis = 150
+ TransitionManager.beginDelayedTransition(root, transition2)
+ view.visibility = View.GONE
+ }
+
+ verify(stateListener2, timeout(3000)).onTransitionStart(any())
+ verify(stateListener2, timeout(3000)).onTransitionEnd(any())
+ verify(stateListener1, times(0)).onTransitionEnd(any())
+ verify(stateListener1, times(0)).onTransitionCancel(any())
+
+ rule.runOnUiThread {
+ // Seek is still controlling the visibility
+ assertThat(view.transitionAlpha).isEqualTo(0.5f)
+ assertThat(view.visibility).isEqualTo(View.VISIBLE)
+
+ seekController.currentPlayTimeMillis = 300
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+ assertThat(view.visibility).isEqualTo(View.GONE)
+ }
+ }
+
+ @Test
+ fun seekCannotTakeOverAnimation() {
+ if (!BuildCompat.isAtLeastU()) return
+ lateinit var seekController: TransitionSeekController
+
+ val stateListener1 = spy(TransitionListenerAdapter())
+ transition.addListener(stateListener1)
+ transition.duration = 1000
+ transition.addListener(TransitionListenerAdapter())
+ rule.runOnUiThread {
+ TransitionManager.beginDelayedTransition(root, transition)
+ view.visibility = View.GONE
+ }
+
+ verify(stateListener1, timeout(3000)).onTransitionStart(any())
+
+ val stateListener2 = spy(TransitionListenerAdapter())
+ val transition2 = Fade()
+ transition2.duration = 3000
+ transition2.addListener(stateListener2)
+
+ rule.runOnUiThread {
+ seekController = TransitionManager.controlDelayedTransition(root, transition2)!!
+ view.visibility = View.GONE
+ }
+
+ rule.runOnUiThread {
+ // The second transition doesn't have any animations in it because it didn't take over.
+ assertThat(seekController.isReady).isTrue()
+ assertThat(seekController.durationMillis).isEqualTo(0)
+ }
+
+ // The first animation should continue
+ verify(stateListener1, timeout(3000)).onTransitionEnd(any())
+
+ // The animation is ended
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+ assertThat(view.visibility).isEqualTo(View.GONE)
+ }
+
+ @Test
+ fun seekCannotTakeOverSeek() {
+ if (!BuildCompat.isAtLeastU()) return
+ lateinit var seekController1: TransitionSeekController
+
+ val stateListener1 = spy(TransitionListenerAdapter())
+ transition.addListener(stateListener1)
+ transition.duration = 3000
+ rule.runOnUiThread {
+ seekController1 = TransitionManager.controlDelayedTransition(root, transition)!!
+ view.visibility = View.GONE
+ }
+
+ rule.runOnUiThread {
+ assertThat(seekController1.isReady).isTrue()
+ }
+
+ val stateListener2 = spy(TransitionListenerAdapter())
+ val transition2 = Fade()
+ transition2.duration = 3000
+ transition2.addListener(stateListener2)
+
+ rule.runOnUiThread {
+ seekController1.currentPlayTimeMillis = 1500
+ // First transition should be started now
+ verify(stateListener1, times(1)).onTransitionStart(any())
+ TransitionManager.controlDelayedTransition(root, transition2)!!
+ view.visibility = View.GONE
+ }
+
+ rule.runOnUiThread {
+ // second transition should just start/end immediately
+ verify(stateListener2, times(1)).onTransitionStart(any())
+ verify(stateListener2, times(1)).onTransitionEnd(any())
+
+ // first transition should still be controllable
+ verify(stateListener1, times(0)).onTransitionEnd(any())
+
+ // second transition should be ready and taking over the previous animation
+ assertThat(view.transitionAlpha).isEqualTo(0.5f)
+ seekController1.currentPlayTimeMillis = 3000
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+ assertThat(view.visibility).isEqualTo(View.GONE)
+ }
+ }
+
+ @Test
+ fun seekReplacesSeek() {
+ if (!BuildCompat.isAtLeastU()) return
+ lateinit var seekController1: TransitionSeekController
+
+ val stateListener1 = spy(TransitionListenerAdapter())
+ transition.addListener(stateListener1)
+ transition.duration = 3000
+ rule.runOnUiThread {
+ seekController1 = TransitionManager.controlDelayedTransition(root, transition)!!
+ view.visibility = View.GONE
+ }
+
+ verify(stateListener1, timeout(3000)).onTransitionStart(any())
+
+ val stateListener2 = spy(TransitionListenerAdapter())
+ val transition2 = Fade()
+ transition2.duration = 3000
+ transition2.addListener(stateListener2)
+
+ lateinit var seekController2: TransitionSeekController
+ rule.runOnUiThread {
+ seekController1.currentPlayTimeMillis = 1500
+ seekController2 = TransitionManager.controlDelayedTransition(root, transition2)!!
+ view.visibility = View.VISIBLE
+ }
+
+ verify(stateListener2, timeout(3000)).onTransitionStart(any())
+ rule.runOnUiThread {}
+ verify(stateListener1, times(1)).onTransitionEnd(any())
+ assertThat(seekController2.isReady).isTrue()
+
+ rule.runOnUiThread {
+ verify(stateListener2, never()).onTransitionEnd(any())
+ assertThat(view.visibility).isEqualTo(View.VISIBLE)
+ assertThat(view.transitionAlpha).isEqualTo(0.5f)
+ seekController2.currentPlayTimeMillis = 3000
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+ assertThat(view.visibility).isEqualTo(View.VISIBLE)
+ verify(stateListener2, times(1)).onTransitionEnd(any())
+ }
+ }
+
+ @Test
+ fun animateToEnd() {
+ if (!BuildCompat.isAtLeastU()) return
+ lateinit var seekController: TransitionSeekController
+
+ val listener = spy(TransitionListenerAdapter())
+ transition.addListener(listener)
+
+ rule.runOnUiThread {
+ seekController = TransitionManager.controlDelayedTransition(root, transition)!!
+ view.visibility = View.GONE
+ }
+
+ rule.runOnUiThread {
+ seekController.currentPlayTimeMillis = 150
+ seekController.animateToEnd()
+ }
+
+ verify(listener, timeout(3000)).onTransitionEnd(any())
+ rule.runOnUiThread {
+ assertThat(view.visibility).isEqualTo(View.GONE)
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+ val runningTransitions = TransitionManager.getRunningTransitions()
+ assertThat(runningTransitions[root]).isEmpty()
+ }
+ }
+
+ @Test
+ fun animateToStart() {
+ if (!BuildCompat.isAtLeastU()) return
+ lateinit var seekController: TransitionSeekController
+
+ val listener = spy(TransitionListenerAdapter())
+ transition.addListener(listener)
+
+ rule.runOnUiThread {
+ seekController = TransitionManager.controlDelayedTransition(root, transition)!!
+ view.visibility = View.GONE
+ }
+
+ rule.runOnUiThread {
+ seekController.currentPlayTimeMillis = 150
+ seekController.animateToStart()
+ }
+
+ verify(listener, timeout(3000)).onTransitionEnd(any())
+ val listener2 = spy(TransitionListenerAdapter())
+ rule.runOnUiThread {
+ assertThat(view.visibility).isEqualTo(View.VISIBLE)
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+
+ // Now set it back to the original state with a fast transition
+ transition.removeListener(listener)
+ transition.addListener(listener2)
+ transition.duration = 0
+ TransitionManager.beginDelayedTransition(root, transition)
+ view.visibility = View.VISIBLE
+ root.invalidate()
+ }
+ verify(listener2, timeout(3000)).onTransitionStart(any())
+ rule.runOnUiThread {
+ verify(listener2, times(1)).onTransitionEnd(any())
+ // All transitions should be ended
+ val runningTransitions = TransitionManager.getRunningTransitions()
+ assertThat(runningTransitions[root]).isEmpty()
+ }
+ }
+
+ @Test
+ fun animateToStartAfterAnimateToEnd() {
+ if (!BuildCompat.isAtLeastU()) return
+ lateinit var seekController: TransitionSeekController
+
+ val listener = spy(TransitionListenerAdapter())
+ transition.addListener(listener)
+
+ rule.runOnUiThread {
+ seekController = TransitionManager.controlDelayedTransition(root, transition)!!
+ view.visibility = View.GONE
+ }
+
+ rule.runOnUiThread {
+ seekController.currentPlayTimeMillis = 150
+ seekController.animateToEnd()
+ }
+
+ rule.runOnUiThread {
+ seekController.animateToStart()
+ }
+
+ verify(listener, timeout(3000)).onTransitionEnd(any())
+
+ rule.runOnUiThread {
+ assertThat(view.visibility).isEqualTo(View.VISIBLE)
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+ }
+ }
+
+ @Test
+ fun animateToEndAfterAnimateToStart() {
+ if (!BuildCompat.isAtLeastU()) return
+ lateinit var seekController: TransitionSeekController
+
+ val listener = spy(TransitionListenerAdapter())
+ transition.addListener(listener)
+
+ rule.runOnUiThread {
+ seekController = TransitionManager.controlDelayedTransition(root, transition)!!
+ view.visibility = View.GONE
+ }
+
+ rule.runOnUiThread {
+ seekController.currentPlayTimeMillis = 150
+ seekController.animateToStart()
+ }
+
+ rule.runOnUiThread {
+ seekController.animateToEnd()
+ }
+
+ verify(listener, timeout(3000)).onTransitionEnd(any())
+
+ rule.runOnUiThread {
+ assertThat(view.visibility).isEqualTo(View.GONE)
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+ }
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun seekAfterAnimate() {
+ if (!BuildCompat.isAtLeastU()) throw IllegalStateException("Not supported before U")
+ lateinit var seekController: TransitionSeekController
+ transition.duration = 5000
+
+ rule.runOnUiThread {
+ seekController = TransitionManager.controlDelayedTransition(root, transition)!!
+ view.visibility = View.GONE
+ }
+
+ rule.runOnUiThread {
+ seekController.currentPlayTimeMillis = 150
+ seekController.animateToEnd()
+ }
+
+ rule.runOnUiThread {
+ seekController.currentPlayTimeMillis = 120
+ }
+ }
+
+ @Test
+ fun seekTransitionSet() {
+ if (!BuildCompat.isAtLeastU()) return
+ transition = TransitionSet().also {
+ it.addTransition(Fade(Fade.MODE_OUT))
+ .addTransition(Fade(Fade.MODE_IN))
+ .ordering = TransitionSet.ORDERING_SEQUENTIAL
+ }
+ val view2 = View(root.context)
+ view2.setBackgroundColor(Color.GREEN)
+
+ val view3 = View(root.context)
+ view3.setBackgroundColor(Color.RED)
+
+ rule.runOnUiThread {
+ root.addView(view2, ViewGroup.LayoutParams(100, 100))
+ root.addView(view3, ViewGroup.LayoutParams(100, 100))
+ view2.visibility = View.GONE
+ }
+
+ lateinit var seekController: TransitionSeekController
+
+ rule.runOnUiThread {
+ seekController = TransitionManager.controlDelayedTransition(root, transition)!!
+ view2.visibility = View.VISIBLE
+ view.visibility = View.GONE
+ }
+
+ rule.runOnUiThread {
+ assertThat(seekController.durationMillis).isEqualTo(600)
+ assertThat(seekController.currentPlayTimeMillis).isEqualTo(0)
+ seekController.currentPlayTimeMillis = 0
+
+ // We should be at the start
+ assertThat(view.visibility).isEqualTo(View.VISIBLE)
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+ assertThat(view2.visibility).isEqualTo(View.VISIBLE)
+ assertThat(view2.transitionAlpha).isEqualTo(0f)
+
+ // seek to the end of the fade out
+ seekController.currentPlayTimeMillis = 300
+
+ assertThat(view.visibility).isEqualTo(View.GONE)
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+ assertThat(view2.visibility).isEqualTo(View.VISIBLE)
+ assertThat(view2.transitionAlpha).isEqualTo(0f)
+
+ // seek to the end of transition
+ seekController.currentPlayTimeMillis = 600
+ assertThat(view.visibility).isEqualTo(View.GONE)
+ assertThat(view2.visibility).isEqualTo(View.VISIBLE)
+ assertThat(view2.transitionAlpha).isEqualTo(1f)
+
+ // seek back to the middle
+ seekController.currentPlayTimeMillis = 300
+
+ assertThat(view.visibility).isEqualTo(View.GONE)
+ assertThat(view2.visibility).isEqualTo(View.VISIBLE)
+ assertThat(view2.transitionAlpha).isEqualTo(0f)
+
+ // back to the beginning
+ seekController.currentPlayTimeMillis = 0
+
+ assertThat(view.visibility).isEqualTo(View.VISIBLE)
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+ assertThat(view2.visibility).isEqualTo(View.VISIBLE)
+ assertThat(view2.transitionAlpha).isEqualTo(0f)
+ }
+ }
+
+ @Test
+ fun animateToEndTransitionSet() {
+ if (!BuildCompat.isAtLeastU()) return
+ transition = TransitionSet().also {
+ it.addTransition(Fade(Fade.MODE_OUT))
+ .addTransition(Fade(Fade.MODE_IN))
+ .ordering = TransitionSet.ORDERING_SEQUENTIAL
+ }
+ val listener = spy(TransitionListenerAdapter())
+ transition.addListener(listener)
+
+ val view2 = View(root.context)
+ view2.setBackgroundColor(Color.GREEN)
+
+ val view3 = View(root.context)
+ view3.setBackgroundColor(Color.RED)
+
+ rule.runOnUiThread {
+ root.addView(view2, ViewGroup.LayoutParams(100, 100))
+ root.addView(view3, ViewGroup.LayoutParams(100, 100))
+ view2.visibility = View.GONE
+ }
+
+ lateinit var seekController: TransitionSeekController
+
+ rule.runOnUiThread {
+ seekController = TransitionManager.controlDelayedTransition(root, transition)!!
+ view2.visibility = View.VISIBLE
+ view.visibility = View.GONE
+ }
+
+ rule.runOnUiThread {
+ verify(listener, times(0)).onTransitionEnd(any())
+ // seek to the end of the fade out
+ seekController.currentPlayTimeMillis = 300
+ verify(listener, times(0)).onTransitionEnd(any())
+
+ seekController.animateToEnd()
+ }
+ verify(listener, timeout(3000)).onTransitionEnd(any())
+
+ rule.runOnUiThread {
+ assertThat(view.visibility).isEqualTo(View.GONE)
+ assertThat(view2.visibility).isEqualTo(View.VISIBLE)
+ assertThat(view2.transitionAlpha).isEqualTo(1f)
+ val runningTransitions = TransitionManager.getRunningTransitions()
+ assertThat(runningTransitions[root]).isEmpty()
+ }
+ }
+
+ @Test
+ fun animateToStartTransitionSet() {
+ if (!BuildCompat.isAtLeastU()) return
+ transition = TransitionSet().also {
+ it.addTransition(Fade(Fade.MODE_OUT))
+ .addTransition(Fade(Fade.MODE_IN))
+ .ordering = TransitionSet.ORDERING_SEQUENTIAL
+ }
+ val listener = spy(TransitionListenerAdapter())
+ transition.addListener(listener)
+
+ val view2 = View(root.context)
+ view2.setBackgroundColor(Color.GREEN)
+
+ val view3 = View(root.context)
+ view3.setBackgroundColor(Color.RED)
+
+ rule.runOnUiThread {
+ root.addView(view2, ViewGroup.LayoutParams(100, 100))
+ root.addView(view3, ViewGroup.LayoutParams(100, 100))
+ view2.visibility = View.GONE
+ }
+
+ lateinit var seekController: TransitionSeekController
+
+ rule.runOnUiThread {
+ seekController = TransitionManager.controlDelayedTransition(root, transition)!!
+ view2.visibility = View.VISIBLE
+ view.visibility = View.GONE
+ }
+
+ rule.runOnUiThread {
+ // seek to near the end of the fade out
+ seekController.currentPlayTimeMillis = 299
+
+ seekController.animateToStart()
+ }
+ verify(listener, timeout(3000)).onTransitionEnd(any(), eq(true))
+ verify(listener, never()).onTransitionEnd(any(), eq(false))
+ verify(listener, times(1)).onTransitionEnd(any(), eq(true))
+
+ val transition2 = TransitionSet().also {
+ it.addTransition(Fade(Fade.MODE_OUT))
+ .addTransition(Fade(Fade.MODE_IN))
+ .ordering = TransitionSet.ORDERING_SEQUENTIAL
+ it.duration = 0
+ }
+
+ val listener2 = spy(TransitionListenerAdapter())
+ transition2.addListener(listener2)
+ rule.runOnUiThread {
+ TransitionManager.beginDelayedTransition(root, transition2)
+ view.visibility = View.VISIBLE
+ view2.visibility = View.GONE
+ }
+ verify(listener2, timeout(3000)).onTransitionStart(any(), eq(false))
+
+ rule.runOnUiThread {
+ verify(listener, times(1)).onTransitionCancel(any())
+ verify(listener, times(1)).onTransitionEnd(any(), eq(false))
+ verify(listener2, times(1)).onTransitionEnd(any())
+ val runningTransitions = TransitionManager.getRunningTransitions()
+ assertThat(runningTransitions[root]).isEmpty()
+ }
+ }
+
+ @Test
+ fun cancelPartOfTransitionSet() {
+ if (!BuildCompat.isAtLeastU()) return
+ transition = TransitionSet().also {
+ it.addTransition(Fade(Fade.MODE_OUT))
+ .addTransition(Fade(Fade.MODE_IN))
+ .ordering = TransitionSet.ORDERING_SEQUENTIAL
+ }
+ val listener = spy(TransitionListenerAdapter())
+ transition.addListener(listener)
+
+ val view2 = View(root.context)
+ view2.setBackgroundColor(Color.GREEN)
+
+ val view3 = View(root.context)
+ view3.setBackgroundColor(Color.RED)
+
+ rule.runOnUiThread {
+ root.addView(view2, ViewGroup.LayoutParams(100, 100))
+ root.addView(view3, ViewGroup.LayoutParams(100, 100))
+ view2.visibility = View.GONE
+ }
+
+ lateinit var seekController: TransitionSeekController
+
+ rule.runOnUiThread {
+ seekController = TransitionManager.controlDelayedTransition(root, transition)!!
+ view2.visibility = View.VISIBLE
+ view.visibility = View.GONE
+ }
+
+ val transition2 = TransitionSet().also {
+ it.addTransition(Fade(Fade.MODE_OUT))
+ .addTransition(Fade(Fade.MODE_IN))
+ .ordering = TransitionSet.ORDERING_SEQUENTIAL
+ }
+
+ val listener2 = spy(TransitionListenerAdapter())
+ transition2.addListener(listener2)
+
+ rule.runOnUiThread {
+ // seek to the end of the fade out
+ seekController.currentPlayTimeMillis = 300
+ TransitionManager.beginDelayedTransition(root, transition2)
+ // Undo making the view visible
+ view2.visibility = View.GONE
+ }
+ verify(listener2, timeout(3000)).onTransitionStart(any())
+ verify(listener2, timeout(3000)).onTransitionEnd(any())
+
+ // The first transition shouldn't end. You can still control it.
+ verify(listener, times(0)).onTransitionEnd(any())
+
+ rule.runOnUiThread {
+ // view2 should now be gone
+ assertThat(view2.visibility).isEqualTo(View.GONE)
+ assertThat(view2.transitionAlpha).isEqualTo(1f)
+
+ // Try to seek further. It should not affect view2 because that transition should be
+ // canceled.
+ seekController.currentPlayTimeMillis = 600
+ assertThat(view2.visibility).isEqualTo(View.GONE)
+ assertThat(view2.transitionAlpha).isEqualTo(1f)
+
+ verify(listener, times(1)).onTransitionEnd(any())
+ }
+ }
+
+ @Test
+ fun onTransitionCallsForwardAndReversed() {
+ if (!BuildCompat.isAtLeastU()) return
+ val listener = spy(TransitionListenerAdapter())
+ transition = Fade()
+ transition.addListener(listener)
+
+ lateinit var seekController: TransitionSeekController
+ rule.runOnUiThread {
+ seekController = TransitionManager.controlDelayedTransition(root, transition)!!
+ view.visibility = View.GONE
+ }
+ rule.runOnUiThread {
+ verifyCallCounts(listener, startForward = 1)
+ seekController.currentPlayTimeMillis = 300
+ verifyCallCounts(listener, startForward = 1, endForward = 1)
+ seekController.currentPlayTimeMillis = 150
+ verifyCallCounts(listener, startForward = 1, endForward = 1, startReverse = 1)
+ seekController.currentPlayTimeMillis = 0
+ verifyCallCounts(
+ listener,
+ startForward = 1,
+ endForward = 1,
+ startReverse = 1,
+ endReverse = 1
+ )
+ }
+ }
+
+ @Test
+ fun onTransitionCallsForwardAndReversedTransitionSet() {
+ if (!BuildCompat.isAtLeastU()) return
+ val fadeOut = Fade(Fade.MODE_OUT)
+ val outListener = spy(TransitionListenerAdapter())
+ fadeOut.addListener(outListener)
+ val fadeIn = Fade(Fade.MODE_IN)
+ val inListener = spy(TransitionListenerAdapter())
+ fadeIn.addListener(inListener)
+ val set = TransitionSet()
+ set.addTransition(fadeOut)
+ set.addTransition(fadeIn)
+ set.setOrdering(TransitionSet.ORDERING_SEQUENTIAL)
+ val setListener = spy(TransitionListenerAdapter())
+ set.addListener(setListener)
+
+ val view2 = View(view.context)
+
+ lateinit var seekController: TransitionSeekController
+ rule.runOnUiThread {
+ seekController = TransitionManager.controlDelayedTransition(root, set)!!
+ view.visibility = View.GONE
+ root.addView(view2, ViewGroup.LayoutParams(100, 100))
+ }
+
+ rule.runOnUiThread {
+ verifyCallCounts(setListener, startForward = 1)
+ verifyCallCounts(outListener, startForward = 1)
+ verifyCallCounts(inListener)
+ seekController.currentPlayTimeMillis = 301
+ verifyCallCounts(setListener, startForward = 1)
+ verifyCallCounts(outListener, startForward = 1, endForward = 1)
+ verifyCallCounts(inListener, startForward = 1)
+ seekController.currentPlayTimeMillis = 600
+ verifyCallCounts(setListener, startForward = 1, endForward = 1)
+ verifyCallCounts(outListener, startForward = 1, endForward = 1)
+ verifyCallCounts(inListener, startForward = 1, endForward = 1)
+ seekController.currentPlayTimeMillis = 301
+ verifyCallCounts(setListener, startForward = 1, endForward = 1, startReverse = 1)
+ verifyCallCounts(outListener, startForward = 1, endForward = 1)
+ verifyCallCounts(inListener, startForward = 1, endForward = 1, startReverse = 1)
+ seekController.currentPlayTimeMillis = 299
+ verifyCallCounts(setListener, startForward = 1, endForward = 1, startReverse = 1)
+ verifyCallCounts(outListener, startForward = 1, endForward = 1, startReverse = 1)
+ verifyCallCounts(
+ inListener,
+ startForward = 1,
+ endForward = 1,
+ startReverse = 1,
+ endReverse = 1
+ )
+ seekController.currentPlayTimeMillis = 0
+ verifyCallCounts(
+ setListener,
+ startForward = 1,
+ endForward = 1,
+ startReverse = 1,
+ endReverse = 1
+ )
+ verifyCallCounts(
+ outListener,
+ startForward = 1,
+ endForward = 1,
+ startReverse = 1,
+ endReverse = 1
+ )
+ verifyCallCounts(
+ inListener,
+ startForward = 1,
+ endForward = 1,
+ startReverse = 1,
+ endReverse = 1
+ )
+ }
+ }
+
+ private fun verifyCallCounts(
+ listener: TransitionListener,
+ startForward: Int = 0,
+ endForward: Int = 0,
+ startReverse: Int = 0,
+ endReverse: Int = 0
+ ) {
+ verify(listener, times(startForward)).onTransitionStart(any(), eq(false))
+ verify(listener, times(endForward)).onTransitionEnd(any(), eq(false))
+ verify(listener, times(startReverse)).onTransitionStart(any(), eq(true))
+ verify(listener, times(endReverse)).onTransitionEnd(any(), eq(true))
+ }
+
+ @Test
+ fun pauseResumeOnSeek() {
+ if (!BuildCompat.isAtLeastU()) return
+ var pauseCount = 0
+ var resumeCount = 0
+ var setPauseCount = 0
+ var setResumeCount = 0
+ val set = TransitionSet().also {
+ it.addTransition(Fade().apply {
+ addListener(object : TransitionListenerAdapter() {
+ override fun onTransitionPause(transition: Transition) {
+ pauseCount++
+ }
+
+ override fun onTransitionResume(transition: Transition) {
+ resumeCount++
+ }
+ })
+ })
+ it.addListener(object : TransitionListenerAdapter() {
+ override fun onTransitionPause(transition: Transition) {
+ setPauseCount++
+ }
+
+ override fun onTransitionResume(transition: Transition) {
+ setResumeCount++
+ }
+ })
+ }
+ transition = TransitionSet().also {
+ it.addTransition(AlwaysTransition("before"))
+ it.addTransition(set)
+ it.setOrdering(TransitionSet.ORDERING_SEQUENTIAL)
+ }
+
+ lateinit var seekController: TransitionSeekController
+ lateinit var view: View
+
+ rule.runOnUiThread {
+ seekController = TransitionManager.controlDelayedTransition(root, transition)!!
+ view = View(rule.activity)
+ root.addView(view, ViewGroup.LayoutParams(100, 100))
+ }
+
+ rule.runOnUiThread {
+ assertThat(view.visibility).isEqualTo(View.VISIBLE)
+ assertThat(view.transitionAlpha).isEqualTo(0f)
+
+ // Move it to the end and then back to the beginning:
+ seekController.currentPlayTimeMillis = 600
+ seekController.currentPlayTimeMillis = 0
+
+ seekController = TransitionManager.controlDelayedTransition(root, transition)!!
+ view.visibility = View.GONE
+ }
+
+ rule.runOnUiThread {
+ assertThat(pauseCount).isEqualTo(1)
+ assertThat(resumeCount).isEqualTo(1)
+ assertThat(setPauseCount).isEqualTo(1)
+ assertThat(setResumeCount).isEqualTo(1)
+ }
+ }
+}
diff --git a/transition/transition/src/androidTest/java/androidx/transition/SlideEdgeTest.java b/transition/transition/src/androidTest/java/androidx/transition/SlideEdgeTest.java
index 7858f8c..e0af8b2 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/SlideEdgeTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/SlideEdgeTest.java
@@ -19,23 +19,29 @@
import static androidx.transition.AtLeastOnceWithin.atLeastOnceWithin;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalMatchers.and;
import static org.mockito.AdditionalMatchers.eq;
import static org.mockito.AdditionalMatchers.gt;
import static org.mockito.AdditionalMatchers.lt;
import static org.mockito.AdditionalMatchers.not;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import android.graphics.Color;
+import android.os.Build;
import android.view.Gravity;
import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.LinearInterpolator;
+import androidx.core.os.BuildCompat;
import androidx.core.util.Pair;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -81,7 +87,7 @@
int slideEdge = SLIDE_EDGES.get(i).first;
final Slide slide = new Slide(slideEdge);
final Transition.TransitionListener listener =
- mock(Transition.TransitionListener.class);
+ spy(new TransitionListenerAdapter());
slide.addListener(listener);
final View redSquare = spy(new View(rule.getActivity()));
@@ -149,7 +155,7 @@
int slideEdge = pair.first;
final Slide slide = new Slide(slideEdge);
final Transition.TransitionListener listener =
- mock(Transition.TransitionListener.class);
+ spy(new TransitionListenerAdapter());
slide.addListener(listener);
@@ -212,6 +218,249 @@
}
}
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void seekingSlideOut() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ transition.addTransition(new Slide());
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View redSquare = new View(activity);
+ redSquare.setBackgroundColor(Color.RED);
+ rule.runOnUiThread(() -> {
+ mRoot.addView(redSquare, new ViewGroup.LayoutParams(100, 100));
+ });
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ redSquare.setVisibility(View.GONE);
+ });
+
+ final TransitionSeekController seekController = seekControllerArr[0];
+
+ float[] translationValues = new float[1];
+
+ rule.runOnUiThread(() -> {
+ assertEquals(1f, ViewUtils.getTransitionAlpha(redSquare), 0f);
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(0f, redSquare.getTranslationX(), 0f);
+ assertEquals(0f, redSquare.getTranslationY(), 0f);
+
+ // Seek past the always there transition before the slide
+ seekController.setCurrentPlayTimeMillis(300);
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(0f, redSquare.getTranslationX(), 0f);
+ assertEquals(0f, redSquare.getTranslationY(), 0f);
+
+ // Seek half way:
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(0f, redSquare.getTranslationX(), 0.01f);
+ assertNotEquals(0f, redSquare.getTranslationY(), 0.01f);
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ translationValues[0] = redSquare.getTranslationY();
+
+ // Seek past the end
+ seekController.setCurrentPlayTimeMillis(800);
+ assertEquals(0f, redSquare.getTranslationX(), 0f);
+ assertEquals(0f, redSquare.getTranslationY(), 0f);
+ assertEquals(View.GONE, redSquare.getVisibility());
+
+ // Seek before the slide:
+ seekController.setCurrentPlayTimeMillis(250);
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(0f, redSquare.getTranslationX(), 0f);
+ assertEquals(0f, redSquare.getTranslationY(), 0f);
+
+ seekController.setCurrentPlayTimeMillis(450);
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, new Slide());
+ redSquare.setVisibility(View.VISIBLE);
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should start from half way values and decrease
+ assertEquals(0, redSquare.getTranslationX(), 0.01f);
+ assertEquals(translationValues[0], redSquare.getTranslationY(), 1f);
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+
+ seekControllerArr[0].setCurrentPlayTimeMillis(300);
+
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(0f, redSquare.getTranslationX(), 0f);
+ assertEquals(0f, redSquare.getTranslationY(), 0f);
+ });
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void seekingSlideIn() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+
+ TransitionSet transition = new TransitionSet();
+ transition.addTransition(new AlwaysTransition("before"));
+ Slide slide = new Slide();
+ slide.setInterpolator(new LinearInterpolator());
+ transition.addTransition(slide);
+ transition.addTransition(new AlwaysTransition("after"));
+ transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
+
+ View redSquare = new View(activity);
+ redSquare.setBackgroundColor(Color.RED);
+ rule.runOnUiThread(() -> {
+ mRoot.addView(redSquare, new ViewGroup.LayoutParams(100, 100));
+ redSquare.setVisibility(View.GONE);
+ });
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, transition);
+ redSquare.setVisibility(View.VISIBLE);
+ });
+
+ final TransitionSeekController seekController = seekControllerArr[0];
+
+ float[] translationValues = new float[1];
+
+ rule.runOnUiThread(() -> {
+ assertEquals(1f, ViewUtils.getTransitionAlpha(redSquare), 0f);
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(0f, redSquare.getTranslationX(), 0.01f);
+ assertTrue(redSquare.getTranslationY() >= mRoot.getHeight());
+
+ float startY = redSquare.getTranslationY();
+
+ // Seek past the always there transition before the slide
+ seekController.setCurrentPlayTimeMillis(300);
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(0f, redSquare.getTranslationX(), 0f);
+ assertEquals(startY, redSquare.getTranslationY(), 0f);
+
+ // Seek half way:
+ seekController.setCurrentPlayTimeMillis(450);
+ assertEquals(0f, redSquare.getTranslationX(), 0.01f);
+ assertEquals(startY / 2f, redSquare.getTranslationY(), 0.01f);
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ translationValues[0] = redSquare.getTranslationY();
+
+ // Seek past the end
+ seekController.setCurrentPlayTimeMillis(800);
+ assertEquals(0f, redSquare.getTranslationX(), 0f);
+ assertEquals(0f, redSquare.getTranslationY(), 0f);
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+
+ // Seek before the slide:
+ seekController.setCurrentPlayTimeMillis(250);
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(0, redSquare.getTranslationX(), 0f);
+ assertEquals(startY, redSquare.getTranslationY(), 0f);
+
+ seekController.setCurrentPlayTimeMillis(450);
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, new Slide());
+ redSquare.setVisibility(View.GONE);
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should start from half way values and increase
+ assertEquals(0f, redSquare.getTranslationX(), 0.01f);
+ assertEquals(translationValues[0], redSquare.getTranslationY(), 1f);
+
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+
+ seekControllerArr[0].setCurrentPlayTimeMillis(300);
+ assertEquals(View.GONE, redSquare.getVisibility());
+ assertEquals(0f, redSquare.getTranslationX(), 0f);
+ assertEquals(0f, redSquare.getTranslationY(), 0f);
+ });
+ }
+
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ public void seekWithTranslation() throws Throwable {
+ if (!BuildCompat.isAtLeastU()) {
+ return; // only supported on U+
+ }
+ final TransitionActivity activity = rule.getActivity();
+ TransitionSeekController[] seekControllerArr = new TransitionSeekController[1];
+ View redSquare = new View(activity);
+ redSquare.setBackgroundColor(Color.RED);
+ rule.runOnUiThread(() -> {
+ mRoot.addView(redSquare, new ViewGroup.LayoutParams(100, 100));
+ redSquare.setTranslationX(1f);
+ redSquare.setTranslationY(5f);
+ });
+
+ rule.runOnUiThread(() -> {
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, new Slide());
+ redSquare.setVisibility(View.GONE);
+ });
+
+ final float[] interruptedTranslation = new float[1];
+
+ rule.runOnUiThread(() -> {
+ assertEquals(1f, redSquare.getTranslationX(), 0.01f);
+ assertEquals(5f, redSquare.getTranslationY(), 0.01f);
+
+
+ seekControllerArr[0].setCurrentPlayTimeMillis(150);
+ interruptedTranslation[0] = redSquare.getTranslationY();
+
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, new Slide());
+ redSquare.setVisibility(View.VISIBLE);
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should start from half way values and increase
+ assertEquals(1f, redSquare.getTranslationX(), 0.01f);
+ assertEquals(interruptedTranslation[0], redSquare.getTranslationY(), 1f);
+
+ // make sure it would go to the start value
+ seekControllerArr[0].setCurrentPlayTimeMillis(300);
+ assertEquals(1f, redSquare.getTranslationX(), 0.01f);
+ assertEquals(5f, redSquare.getTranslationY(), 0.01f);
+
+ // Now go back to the interrupted position again:
+ seekControllerArr[0].setCurrentPlayTimeMillis(0);
+ assertEquals(1f, redSquare.getTranslationX(), 0.01f);
+ assertEquals(interruptedTranslation[0], redSquare.getTranslationY(), 1f);
+
+ // Send it back to GONE
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, new Slide());
+ redSquare.setVisibility(View.GONE);
+ });
+
+ rule.runOnUiThread(() -> {
+ assertEquals(1f, redSquare.getTranslationX(), 0.01f);
+ assertEquals(interruptedTranslation[0], redSquare.getTranslationY(), 1f);
+
+ // it should move away (toward the top-left)
+ seekControllerArr[0].setCurrentPlayTimeMillis(299);
+ assertEquals(1f, redSquare.getTranslationX(), 0.01f);
+ assertTrue(redSquare.getTranslationY() > interruptedTranslation[0]);
+
+ seekControllerArr[0] = TransitionManager.controlDelayedTransition(mRoot, new Slide());
+ redSquare.setVisibility(View.VISIBLE);
+ });
+
+ rule.runOnUiThread(() -> {
+ // It should end up at the initial translation
+ seekControllerArr[0].setCurrentPlayTimeMillis(300);
+ assertEquals(1f, redSquare.getTranslationX(), 0.01f);
+ assertEquals(5f, redSquare.getTranslationY(), 0.01f);
+ });
+ }
+
private void verifyNoTranslation(View view) {
assertEquals(0f, view.getTranslationX(), 0.01f);
assertEquals(0f, view.getTranslationY(), 0.01f);
diff --git a/transition/transition/src/androidTest/java/androidx/transition/TransitionActivity.java b/transition/transition/src/androidTest/java/androidx/transition/TransitionActivity.java
index 002c7d4..86f7240 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/TransitionActivity.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/TransitionActivity.java
@@ -41,6 +41,7 @@
overridePendingTransition(0, 0);
}
+ @SuppressWarnings("deprecation")
@Override
public void finish() {
super.finish();
diff --git a/transition/transition/src/androidTest/java/androidx/transition/TransitionManagerTest.java b/transition/transition/src/androidTest/java/androidx/transition/TransitionManagerTest.java
index 3d4397e..c80073a 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/TransitionManagerTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/TransitionManagerTest.java
@@ -21,8 +21,8 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.sameInstance;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
@@ -181,7 +181,7 @@
final Transition transition = new AutoTransition();
// This transition is very long, but will be forced to end as soon as it starts
transition.setDuration(30000);
- final Transition.TransitionListener listener = mock(Transition.TransitionListener.class);
+ final Transition.TransitionListener listener = spy(new TransitionListenerAdapter());
transition.addListener(listener);
rule.runOnUiThread(new Runnable() {
@Override
@@ -204,7 +204,7 @@
final ViewGroup root = rule.getActivity().getRoot();
final Transition transition = new AutoTransition();
transition.setDuration(0);
- final Transition.TransitionListener listener = mock(Transition.TransitionListener.class);
+ final Transition.TransitionListener listener = spy(new TransitionListenerAdapter());
transition.addListener(listener);
rule.runOnUiThread(new Runnable() {
@Override
diff --git a/transition/transition/src/androidTest/java/androidx/transition/TransitionTest.java b/transition/transition/src/androidTest/java/androidx/transition/TransitionTest.java
index 4c586a5..30a88f5 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/TransitionTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/TransitionTest.java
@@ -35,6 +35,7 @@
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
import android.graphics.Rect;
import android.view.LayoutInflater;
import android.view.View;
@@ -377,6 +378,61 @@
assertThat(transition.isTransitionRequired(start, end), is(true));
}
+ // Any listener that is added by the transition itself should not be in the global set of
+ // listeners. They should be limited to the executing transition.
+ @Test
+ public void internalListenersNotGlobal() throws Throwable {
+ rule.runOnUiThread(() -> {
+ mScenes[0].enter();
+ });
+ View view = rule.getActivity().findViewById(R.id.view0);
+
+ int[] startCount = new int[1];
+ Transition transition = new Visibility() {
+ private Animator createAnimator() {
+ addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionStart(@NonNull Transition transition) {
+ startCount[0]++;
+ }
+ });
+ return ValueAnimator.ofFloat(0f, 100f);
+ }
+
+ @Nullable
+ @Override
+ public Animator onDisappear(@NonNull ViewGroup sceneRoot, @NonNull View view,
+ @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) {
+ return createAnimator();
+ }
+
+ @Nullable
+ @Override
+ public Animator onAppear(@NonNull ViewGroup sceneRoot, @NonNull View view,
+ @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) {
+ return createAnimator();
+ }
+ };
+
+ rule.runOnUiThread(() -> {
+ ViewGroup root = rule.getActivity().getRoot();
+ TransitionManager.beginDelayedTransition(root, transition);
+ view.setVisibility(View.GONE);
+ });
+
+ rule.runOnUiThread(() -> {
+ assertEquals(1, startCount[0]);
+
+ ViewGroup root = rule.getActivity().getRoot();
+ TransitionManager.beginDelayedTransition(root, transition);
+ view.setVisibility(View.VISIBLE);
+ });
+
+ rule.runOnUiThread(() -> {
+ assertEquals(2, startCount[0]);
+ });
+ }
+
private void showInitialScene() throws Throwable {
SyncRunnable enter0 = new SyncRunnable();
mScenes[0].setEnterAction(enter0);
diff --git a/transition/transition/src/androidTest/java/androidx/transition/TranslationAnimationCreatorTest.java b/transition/transition/src/androidTest/java/androidx/transition/TranslationAnimationCreatorTest.java
index 976277a..207880f 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/TranslationAnimationCreatorTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/TranslationAnimationCreatorTest.java
@@ -53,7 +53,7 @@
assertEquals(20, view.getTranslationX(), 0.01);
verify(transition).addListener(listenerCaptor.capture());
- listenerCaptor.getValue().onTransitionEnd(transition);
+ listenerCaptor.getValue().onTransitionEnd(transition, false);
// but onTransitionEnd does
assertEquals(0, view.getTranslationX(), 0.01);
}
diff --git a/transition/transition/src/androidTest/java/androidx/transition/VisibilityTest.java b/transition/transition/src/androidTest/java/androidx/transition/VisibilityTest.java
index c433a98..73ce42f 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/VisibilityTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/VisibilityTest.java
@@ -23,7 +23,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
@@ -124,7 +124,7 @@
return ValueAnimator.ofFloat(0, 1);
}
});
- Transition.TransitionListener listener = mock(Transition.TransitionListener.class);
+ Transition.TransitionListener listener = spy(new TransitionListenerAdapter());
set.addListener(listener);
// remove view
@@ -177,7 +177,7 @@
return ValueAnimator.ofFloat(0, 1);
}
};
- Transition.TransitionListener listener = mock(Transition.TransitionListener.class);
+ Transition.TransitionListener listener = spy(new TransitionListenerAdapter());
visibility.addListener(listener);
// remove view
diff --git a/transition/transition/src/main/java/androidx/transition/ChangeBounds.java b/transition/transition/src/main/java/androidx/transition/ChangeBounds.java
index a0fcc2c..e72333d 100644
--- a/transition/transition/src/main/java/androidx/transition/ChangeBounds.java
+++ b/transition/transition/src/main/java/androidx/transition/ChangeBounds.java
@@ -20,18 +20,13 @@
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Property;
import android.view.View;
@@ -66,24 +61,6 @@
PROPNAME_WINDOW_Y
};
- private static final Property<Drawable, PointF> DRAWABLE_ORIGIN_PROPERTY =
- new Property<Drawable, PointF>(PointF.class, "boundsOrigin") {
- private Rect mBounds = new Rect();
-
- @Override
- public void set(Drawable object, PointF value) {
- object.copyBounds(mBounds);
- mBounds.offsetTo(Math.round(value.x), Math.round(value.y));
- object.setBounds(mBounds);
- }
-
- @Override
- public PointF get(Drawable object) {
- object.copyBounds(mBounds);
- return new PointF(mBounds.left, mBounds.top);
- }
- };
-
private static final Property<ViewBounds, PointF> TOP_LEFT_PROPERTY =
new Property<ViewBounds, PointF>(PointF.class, "topLeft") {
@Override
@@ -161,11 +138,9 @@
}
};
- private int[] mTempLocation = new int[2];
private boolean mResizeClip = false;
- private boolean mReparent = false;
- private static RectEvaluator sRectEvaluator = new RectEvaluator();
+ private static final RectEvaluator sRectEvaluator = new RectEvaluator();
public ChangeBounds() {
}
@@ -182,6 +157,11 @@
setResizeClip(resizeClip);
}
+ @Override
+ public boolean isSeekingSupported() {
+ return true;
+ }
+
@NonNull
@Override
public String[] getTransitionProperties() {
@@ -223,11 +203,6 @@
values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(),
view.getRight(), view.getBottom()));
values.values.put(PROPNAME_PARENT, values.view.getParent());
- if (mReparent) {
- values.view.getLocationInWindow(mTempLocation);
- values.values.put(PROPNAME_WINDOW_X, mTempLocation[0]);
- values.values.put(PROPNAME_WINDOW_Y, mTempLocation[1]);
- }
if (mResizeClip) {
values.values.put(PROPNAME_CLIP, ViewCompat.getClipBounds(view));
}
@@ -237,6 +212,13 @@
@Override
public void captureStartValues(@NonNull TransitionValues transitionValues) {
captureValues(transitionValues);
+ if (mResizeClip) {
+ Rect clipSize =
+ (Rect) transitionValues.view.getTag(R.id.transition_clip);
+ if (clipSize != null) {
+ transitionValues.values.put(PROPNAME_CLIP, clipSize);
+ }
+ }
}
@Override
@@ -244,19 +226,6 @@
captureValues(transitionValues);
}
- private boolean parentMatches(View startParent, View endParent) {
- boolean parentMatches = true;
- if (mReparent) {
- TransitionValues endValues = getMatchedTransitionValues(startParent, true);
- if (endValues == null) {
- parentMatches = startParent == endParent;
- } else {
- parentMatches = endParent == endValues.view;
- }
- }
- return parentMatches;
- }
-
@Override
@Nullable
public Animator createAnimator(@NonNull final ViewGroup sceneRoot,
@@ -272,188 +241,118 @@
return null;
}
final View view = endValues.view;
- if (parentMatches(startParent, endParent)) {
- Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS);
- Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
- final int startLeft = startBounds.left;
- final int endLeft = endBounds.left;
- final int startTop = startBounds.top;
- final int endTop = endBounds.top;
- final int startRight = startBounds.right;
- final int endRight = endBounds.right;
- final int startBottom = startBounds.bottom;
- final int endBottom = endBounds.bottom;
- final int startWidth = startRight - startLeft;
- final int startHeight = startBottom - startTop;
- final int endWidth = endRight - endLeft;
- final int endHeight = endBottom - endTop;
- Rect startClip = (Rect) startValues.values.get(PROPNAME_CLIP);
- Rect endClip = (Rect) endValues.values.get(PROPNAME_CLIP);
- int numChanges = 0;
- if ((startWidth != 0 && startHeight != 0) || (endWidth != 0 && endHeight != 0)) {
- if (startLeft != endLeft || startTop != endTop) ++numChanges;
- if (startRight != endRight || startBottom != endBottom) ++numChanges;
- }
- if ((startClip != null && !startClip.equals(endClip))
- || (startClip == null && endClip != null)) {
- ++numChanges;
- }
- if (numChanges > 0) {
- Animator anim;
- if (!mResizeClip) {
- ViewUtils.setLeftTopRightBottom(view, startLeft, startTop, startRight,
- startBottom);
- if (numChanges == 2) {
- if (startWidth == endWidth && startHeight == endHeight) {
- Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft,
- endTop);
- anim = ObjectAnimatorUtils.ofPointF(view, POSITION_PROPERTY,
- topLeftPath);
- } else {
- final ViewBounds viewBounds = new ViewBounds(view);
- Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
- endLeft, endTop);
- ObjectAnimator topLeftAnimator = ObjectAnimatorUtils
- .ofPointF(viewBounds, TOP_LEFT_PROPERTY, topLeftPath);
-
- Path bottomRightPath = getPathMotion().getPath(startRight, startBottom,
- endRight, endBottom);
- ObjectAnimator bottomRightAnimator = ObjectAnimatorUtils.ofPointF(
- viewBounds, BOTTOM_RIGHT_PROPERTY, bottomRightPath);
- AnimatorSet set = new AnimatorSet();
- set.playTogether(topLeftAnimator, bottomRightAnimator);
- anim = set;
- set.addListener(new AnimatorListenerAdapter() {
- // We need a strong reference to viewBounds until the
- // animator ends (The ObjectAnimator holds only a weak reference).
- @SuppressWarnings("unused")
- private ViewBounds mViewBounds = viewBounds;
- });
- }
- } else if (startLeft != endLeft || startTop != endTop) {
- Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
- endLeft, endTop);
- anim = ObjectAnimatorUtils.ofPointF(view, TOP_LEFT_ONLY_PROPERTY,
- topLeftPath);
- } else {
- Path bottomRight = getPathMotion().getPath(startRight, startBottom,
- endRight, endBottom);
- anim = ObjectAnimatorUtils.ofPointF(view, BOTTOM_RIGHT_ONLY_PROPERTY,
- bottomRight);
- }
- } else {
- int maxWidth = Math.max(startWidth, endWidth);
- int maxHeight = Math.max(startHeight, endHeight);
-
- ViewUtils.setLeftTopRightBottom(view, startLeft, startTop, startLeft + maxWidth,
- startTop + maxHeight);
-
- ObjectAnimator positionAnimator = null;
- if (startLeft != endLeft || startTop != endTop) {
+ Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS);
+ Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
+ final int startLeft = startBounds.left;
+ final int endLeft = endBounds.left;
+ final int startTop = startBounds.top;
+ final int endTop = endBounds.top;
+ final int startRight = startBounds.right;
+ final int endRight = endBounds.right;
+ final int startBottom = startBounds.bottom;
+ final int endBottom = endBounds.bottom;
+ final int startWidth = startRight - startLeft;
+ final int startHeight = startBottom - startTop;
+ final int endWidth = endRight - endLeft;
+ final int endHeight = endBottom - endTop;
+ Rect startClip = (Rect) startValues.values.get(PROPNAME_CLIP);
+ Rect endClip = (Rect) endValues.values.get(PROPNAME_CLIP);
+ int numChanges = 0;
+ if ((startWidth != 0 && startHeight != 0) || (endWidth != 0 && endHeight != 0)) {
+ if (startLeft != endLeft || startTop != endTop) ++numChanges;
+ if (startRight != endRight || startBottom != endBottom) ++numChanges;
+ }
+ if ((startClip != null && !startClip.equals(endClip))
+ || (startClip == null && endClip != null)) {
+ ++numChanges;
+ }
+ if (numChanges > 0) {
+ Animator anim;
+ if (!mResizeClip) {
+ ViewUtils.setLeftTopRightBottom(view, startLeft, startTop, startRight,
+ startBottom);
+ if (numChanges == 2) {
+ if (startWidth == endWidth && startHeight == endHeight) {
Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft,
endTop);
- positionAnimator = ObjectAnimatorUtils.ofPointF(view, POSITION_PROPERTY,
+ anim = ObjectAnimatorUtils.ofPointF(view, POSITION_PROPERTY,
topLeftPath);
- }
- final Rect finalClip = endClip;
- if (startClip == null) {
- startClip = new Rect(0, 0, startWidth, startHeight);
- }
- if (endClip == null) {
- endClip = new Rect(0, 0, endWidth, endHeight);
- }
- ObjectAnimator clipAnimator = null;
- if (!startClip.equals(endClip)) {
- ViewCompat.setClipBounds(view, startClip);
- clipAnimator = ObjectAnimator.ofObject(view, "clipBounds", sRectEvaluator,
- startClip, endClip);
- clipAnimator.addListener(new AnimatorListenerAdapter() {
- private boolean mIsCanceled;
+ } else {
+ final ViewBounds viewBounds = new ViewBounds(view);
+ Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
+ endLeft, endTop);
+ ObjectAnimator topLeftAnimator = ObjectAnimatorUtils
+ .ofPointF(viewBounds, TOP_LEFT_PROPERTY, topLeftPath);
- @Override
- public void onAnimationCancel(Animator animation) {
- mIsCanceled = true;
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- if (!mIsCanceled) {
- ViewCompat.setClipBounds(view, finalClip);
- ViewUtils.setLeftTopRightBottom(view, endLeft, endTop, endRight,
- endBottom);
- }
- }
+ Path bottomRightPath = getPathMotion().getPath(startRight, startBottom,
+ endRight, endBottom);
+ ObjectAnimator bottomRightAnimator = ObjectAnimatorUtils.ofPointF(
+ viewBounds, BOTTOM_RIGHT_PROPERTY, bottomRightPath);
+ AnimatorSet set = new AnimatorSet();
+ set.playTogether(topLeftAnimator, bottomRightAnimator);
+ anim = set;
+ set.addListener(new AnimatorListenerAdapter() {
+ // We need a strong reference to viewBounds until the
+ // animator ends (The ObjectAnimator holds only a weak reference).
+ @SuppressWarnings("unused")
+ private final ViewBounds mViewBounds = viewBounds;
});
}
- anim = TransitionUtils.mergeAnimators(positionAnimator,
- clipAnimator);
+ } else if (startLeft != endLeft || startTop != endTop) {
+ Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
+ endLeft, endTop);
+ anim = ObjectAnimatorUtils.ofPointF(view, TOP_LEFT_ONLY_PROPERTY,
+ topLeftPath);
+ } else {
+ Path bottomRight = getPathMotion().getPath(startRight, startBottom,
+ endRight, endBottom);
+ anim = ObjectAnimatorUtils.ofPointF(view, BOTTOM_RIGHT_ONLY_PROPERTY,
+ bottomRight);
}
- if (view.getParent() instanceof ViewGroup) {
- final ViewGroup parent = (ViewGroup) view.getParent();
- ViewGroupUtils.suppressLayout(parent, true);
- TransitionListener transitionListener = new TransitionListenerAdapter() {
- boolean mCanceled = false;
+ } else {
+ int maxWidth = Math.max(startWidth, endWidth);
+ int maxHeight = Math.max(startHeight, endHeight);
- @Override
- public void onTransitionCancel(@NonNull Transition transition) {
- ViewGroupUtils.suppressLayout(parent, false);
- mCanceled = true;
- }
+ ViewUtils.setLeftTopRightBottom(view, startLeft, startTop, startLeft + maxWidth,
+ startTop + maxHeight);
- @Override
- public void onTransitionEnd(@NonNull Transition transition) {
- if (!mCanceled) {
- ViewGroupUtils.suppressLayout(parent, false);
- }
- transition.removeListener(this);
- }
-
- @Override
- public void onTransitionPause(@NonNull Transition transition) {
- ViewGroupUtils.suppressLayout(parent, false);
- }
-
- @Override
- public void onTransitionResume(@NonNull Transition transition) {
- ViewGroupUtils.suppressLayout(parent, true);
- }
- };
- addListener(transitionListener);
+ ObjectAnimator positionAnimator = null;
+ if (startLeft != endLeft || startTop != endTop) {
+ Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft,
+ endTop);
+ positionAnimator = ObjectAnimatorUtils.ofPointF(view, POSITION_PROPERTY,
+ topLeftPath);
}
- return anim;
+ boolean startClipIsNull = startClip == null;
+ if (startClipIsNull) {
+ startClip = new Rect(0, 0, startWidth, startHeight);
+ }
+ boolean endClipIsNull = endClip == null;
+ if (endClipIsNull) {
+ endClip = new Rect(0, 0, endWidth, endHeight);
+ }
+ ObjectAnimator clipAnimator = null;
+ if (!startClip.equals(endClip)) {
+ ViewCompat.setClipBounds(view, startClip);
+ clipAnimator = ObjectAnimator.ofObject(view, "clipBounds", sRectEvaluator,
+ startClip, endClip);
+ ClipListener listener = new ClipListener(view,
+ startClip, startClipIsNull, endClip, endClipIsNull,
+ startLeft, startTop, startRight, startBottom,
+ endLeft, endTop, endRight, endBottom
+ );
+ clipAnimator.addListener(listener);
+ addListener(listener);
+ }
+ anim = TransitionUtils.mergeAnimators(positionAnimator,
+ clipAnimator);
}
- } else {
- int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X);
- int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y);
- int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X);
- int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y);
- // TODO: also handle size changes: check bounds and animate size changes
- if (startX != endX || startY != endY) {
- sceneRoot.getLocationInWindow(mTempLocation);
- Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(),
- Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(bitmap);
- view.draw(canvas);
- @SuppressWarnings("deprecation") final BitmapDrawable drawable = new BitmapDrawable(
- bitmap);
- final float transitionAlpha = ViewUtils.getTransitionAlpha(view);
- ViewUtils.setTransitionAlpha(view, 0);
- ViewUtils.getOverlay(sceneRoot).add(drawable);
- Path topLeftPath = getPathMotion().getPath(startX - mTempLocation[0],
- startY - mTempLocation[1], endX - mTempLocation[0],
- endY - mTempLocation[1]);
- PropertyValuesHolder origin = PropertyValuesHolderUtils.ofPointF(
- DRAWABLE_ORIGIN_PROPERTY, topLeftPath);
- ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, origin);
- anim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- ViewUtils.getOverlay(sceneRoot).remove(drawable);
- ViewUtils.setTransitionAlpha(view, transitionAlpha);
- }
- });
- return anim;
+ if (view.getParent() instanceof ViewGroup) {
+ final ViewGroup parent = (ViewGroup) view.getParent();
+ ViewGroupUtils.suppressLayout(parent, true);
+ getRootTransition().addListener(new SuppressLayoutListener(parent));
}
+ return anim;
}
return null;
}
@@ -464,7 +363,7 @@
private int mTop;
private int mRight;
private int mBottom;
- private View mView;
+ private final View mView;
private int mTopLeftCalls;
private int mBottomRightCalls;
@@ -495,7 +394,149 @@
mTopLeftCalls = 0;
mBottomRightCalls = 0;
}
-
}
+ private static class ClipListener
+ extends AnimatorListenerAdapter implements TransitionListener {
+ private final View mView;
+ private final Rect mStartClip;
+ private final boolean mStartClipIsNull;
+ private final Rect mEndClip;
+ private final boolean mEndClipIsNull;
+ private final int mStartLeft, mStartTop, mStartRight, mStartBottom;
+ private final int mEndLeft, mEndTop, mEndRight, mEndBottom;
+
+ private boolean mIsCanceled;
+
+ ClipListener(View view,
+ Rect startClip,
+ boolean startClipIsNull,
+ Rect endClip,
+ boolean endClipIsNull,
+ int startLeft,
+ int startTop,
+ int startRight,
+ int startBottom,
+ int endLeft,
+ int endTop,
+ int endRight,
+ int endBottom
+ ) {
+ mView = view;
+ mStartClip = startClip;
+ mStartClipIsNull = startClipIsNull;
+ mEndClip = endClip;
+ mEndClipIsNull = endClipIsNull;
+ mStartLeft = startLeft;
+ mStartTop = startTop;
+ mStartRight = startRight;
+ mStartBottom = startBottom;
+ mEndLeft = endLeft;
+ mEndTop = endTop;
+ mEndRight = endRight;
+ mEndBottom = endBottom;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ onAnimationStart(animation, false);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ onAnimationEnd(animation, false);
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation, boolean isReverse) {
+ int maxWidth = Math.max(mStartRight - mStartLeft, mEndRight - mEndLeft);
+ int maxHeight = Math.max(mStartBottom - mStartTop, mEndBottom - mEndTop);
+
+ int left = isReverse ? mEndLeft : mStartLeft;
+ int top = isReverse ? mEndTop : mStartTop;
+ ViewUtils.setLeftTopRightBottom(mView, left, top, left + maxWidth, top + maxHeight);
+
+ Rect clip = isReverse ? mEndClip : mStartClip;
+ ViewCompat.setClipBounds(mView, clip);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation, boolean isReverse) {
+ if (mIsCanceled) {
+ return;
+ }
+ Rect clip = isReverse
+ ? (mStartClipIsNull ? null : mStartClip)
+ : (mEndClipIsNull ? null : mEndClip);
+ ViewCompat.setClipBounds(mView, clip);
+ if (isReverse) {
+ ViewUtils.setLeftTopRightBottom(mView, mStartLeft, mStartTop, mStartRight,
+ mStartBottom);
+ } else {
+ ViewUtils.setLeftTopRightBottom(mView, mEndLeft, mEndTop, mEndRight, mEndBottom);
+ }
+ }
+
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ mIsCanceled = true;
+ }
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ Rect pauseClip = ViewCompat.getClipBounds(mView);
+ mView.setTag(R.id.transition_clip, pauseClip);
+ Rect clip = mEndClipIsNull ? null : mEndClip;
+ ViewCompat.setClipBounds(mView, clip);
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ Rect pauseClip = (Rect) mView.getTag(R.id.transition_clip);
+ mView.setTag(R.id.transition_clip, null);
+ ViewCompat.setClipBounds(mView, pauseClip);
+ }
+
+ @Override
+ public void onTransitionStart(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ }
+ }
+
+ private static class SuppressLayoutListener extends TransitionListenerAdapter {
+ boolean mCanceled = false;
+
+ final ViewGroup mParent;
+
+ SuppressLayoutListener(@NonNull ViewGroup parent) {
+ mParent = parent;
+ }
+
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ ViewGroupUtils.suppressLayout(mParent, false);
+ mCanceled = true;
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ if (!mCanceled) {
+ ViewGroupUtils.suppressLayout(mParent, false);
+ }
+ transition.removeListener(this);
+ }
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ ViewGroupUtils.suppressLayout(mParent, false);
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ ViewGroupUtils.suppressLayout(mParent, true);
+ }
+ }
}
diff --git a/transition/transition/src/main/java/androidx/transition/ChangeClipBounds.java b/transition/transition/src/main/java/androidx/transition/ChangeClipBounds.java
index b568ae2..bcd40a3 100644
--- a/transition/transition/src/main/java/androidx/transition/ChangeClipBounds.java
+++ b/transition/transition/src/main/java/androidx/transition/ChangeClipBounds.java
@@ -44,6 +44,10 @@
PROPNAME_CLIP,
};
+ // Represents a null Rect in the tag. If null were used instead, we would treat it
+ // as not set.
+ static final Rect NULL_SENTINEL = new Rect();
+
@Override
@NonNull
public String[] getTransitionProperties() {
@@ -57,13 +61,28 @@
super(context, attrs);
}
- private void captureValues(TransitionValues values) {
+ @Override
+ public boolean isSeekingSupported() {
+ return true;
+ }
+
+ @SuppressWarnings("ReferenceEquality") // Reference comparison with NULL_SENTINEL
+ private void captureValues(TransitionValues values, boolean clipFromTag) {
View view = values.view;
if (view.getVisibility() == View.GONE) {
return;
}
- Rect clip = ViewCompat.getClipBounds(view);
+ Rect clip = null;
+ if (clipFromTag) {
+ clip = (Rect) view.getTag(R.id.transition_clip);
+ }
+ if (clip == null) {
+ clip = ViewCompat.getClipBounds(view);
+ }
+ if (clip == NULL_SENTINEL) {
+ clip = null;
+ }
values.values.put(PROPNAME_CLIP, clip);
if (clip == null) {
Rect bounds = new Rect(0, 0, view.getWidth(), view.getHeight());
@@ -73,12 +92,12 @@
@Override
public void captureStartValues(@NonNull TransitionValues transitionValues) {
- captureValues(transitionValues);
+ captureValues(transitionValues, true);
}
@Override
public void captureEndValues(@NonNull TransitionValues transitionValues) {
- captureValues(transitionValues);
+ captureValues(transitionValues, false);
}
@Nullable
@@ -93,33 +112,83 @@
}
Rect start = (Rect) startValues.values.get(PROPNAME_CLIP);
Rect end = (Rect) endValues.values.get(PROPNAME_CLIP);
- final boolean endIsNull = end == null;
if (start == null && end == null) {
return null; // No animation required since there is no clip.
}
- if (start == null) {
- start = (Rect) startValues.values.get(PROPNAME_BOUNDS);
- } else if (end == null) {
- end = (Rect) endValues.values.get(PROPNAME_BOUNDS);
- }
- if (start.equals(end)) {
+ Rect startClip = start == null ? (Rect) startValues.values.get(PROPNAME_BOUNDS) : start;
+ Rect endClip = end == null ? (Rect) endValues.values.get(PROPNAME_BOUNDS) : end;
+
+ if (startClip.equals(endClip)) {
return null;
}
ViewCompat.setClipBounds(endValues.view, start);
RectEvaluator evaluator = new RectEvaluator(new Rect());
ObjectAnimator animator = ObjectAnimator.ofObject(endValues.view, ViewUtils.CLIP_BOUNDS,
- evaluator, start, end);
- if (endIsNull) {
- final View endView = endValues.view;
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- ViewCompat.setClipBounds(endView, null);
- }
- });
- }
+ evaluator, startClip, endClip);
+ View view = endValues.view;
+ Listener listener = new Listener(view, start, end);
+ animator.addListener(listener);
+ addListener(listener);
return animator;
}
+
+ private static class Listener extends AnimatorListenerAdapter implements TransitionListener {
+ private final Rect mStart;
+ private final Rect mEnd;
+ private final View mView;
+
+ Listener(View view, Rect start, Rect end) {
+ mView = view;
+ mStart = start;
+ mEnd = end;
+ }
+
+ @Override
+ public void onTransitionStart(@NonNull Transition transition) {
+
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+
+ }
+
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+
+ }
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ Rect clipBounds = ViewCompat.getClipBounds(mView);
+ if (clipBounds == null) {
+ clipBounds = NULL_SENTINEL;
+ }
+ mView.setTag(R.id.transition_clip, clipBounds);
+ ViewCompat.setClipBounds(mView, mEnd);
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ Rect clipBounds = (Rect) mView.getTag(R.id.transition_clip);
+ ViewCompat.setClipBounds(mView, clipBounds);
+ mView.setTag(R.id.transition_clip, null);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ onAnimationEnd(animation, false);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation, boolean isReverse) {
+ if (!isReverse) {
+ ViewCompat.setClipBounds(mView, mEnd);
+ } else {
+ ViewCompat.setClipBounds(mView, mStart);
+ }
+ }
+ }
}
diff --git a/transition/transition/src/main/java/androidx/transition/ChangeImageTransform.java b/transition/transition/src/main/java/androidx/transition/ChangeImageTransform.java
index cfdbbd3..1d6b6d0 100644
--- a/transition/transition/src/main/java/androidx/transition/ChangeImageTransform.java
+++ b/transition/transition/src/main/java/androidx/transition/ChangeImageTransform.java
@@ -17,6 +17,7 @@
package androidx.transition;
import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.TypeEvaluator;
import android.content.Context;
@@ -79,7 +80,12 @@
super(context, attrs);
}
- private void captureValues(TransitionValues transitionValues) {
+ @Override
+ public boolean isSeekingSupported() {
+ return true;
+ }
+
+ private void captureValues(TransitionValues transitionValues, boolean useIntermediate) {
View view = transitionValues.view;
if (!(view instanceof ImageView) || view.getVisibility() != View.VISIBLE) {
return;
@@ -98,17 +104,24 @@
Rect bounds = new Rect(left, top, right, bottom);
values.put(PROPNAME_BOUNDS, bounds);
- values.put(PROPNAME_MATRIX, copyImageMatrix(imageView));
+ Matrix matrix = null;
+ if (useIntermediate) {
+ matrix = (Matrix) imageView.getTag(R.id.transition_image_transform);
+ }
+ if (matrix == null) {
+ matrix = copyImageMatrix(imageView);
+ }
+ values.put(PROPNAME_MATRIX, matrix);
}
@Override
public void captureStartValues(@NonNull TransitionValues transitionValues) {
- captureValues(transitionValues);
+ captureValues(transitionValues, true);
}
@Override
public void captureEndValues(@NonNull TransitionValues transitionValues) {
- captureValues(transitionValues);
+ captureValues(transitionValues, false);
}
@Override
@@ -168,6 +181,10 @@
}
ANIMATED_TRANSFORM_PROPERTY.set(imageView, startMatrix);
animator = createMatrixAnimator(imageView, startMatrix, endMatrix);
+ Listener listener = new Listener(imageView, startMatrix, endMatrix);
+ animator.addListener(listener);
+ AnimatorUtils.addPauseListener(animator, listener);
+ addListener(listener);
}
return animator;
@@ -241,4 +258,85 @@
return matrix;
}
+ private static class Listener extends AnimatorListenerAdapter implements TransitionListener,
+ AnimatorUtils.AnimatorPauseListenerCompat {
+ private final ImageView mImageView;
+ private final Matrix mStartMatrix;
+ private final Matrix mEndMatrix;
+ private boolean mIsBeforeAnimator = true;
+
+ Listener(ImageView imageView, Matrix startMatrix, Matrix endMatrix) {
+ mImageView = imageView;
+ mStartMatrix = startMatrix;
+ mEndMatrix = endMatrix;
+ }
+
+ @Override
+ public void onTransitionStart(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ if (mIsBeforeAnimator) {
+ saveMatrix(mStartMatrix);
+ }
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ restoreMatrix();
+ }
+
+ @Override
+ public void onAnimationStart(@NonNull Animator animation, boolean isReverse) {
+ mIsBeforeAnimator = false;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mIsBeforeAnimator = false;
+ }
+
+ @Override
+ public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) {
+ mIsBeforeAnimator = isReverse;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mIsBeforeAnimator = false;
+ }
+
+ @Override
+ public void onAnimationPause(Animator animation) {
+ Matrix pauseMatrix = (Matrix) ((ObjectAnimator) animation).getAnimatedValue();
+ saveMatrix(pauseMatrix);
+ }
+
+ @Override
+ public void onAnimationResume(Animator animation) {
+ restoreMatrix();
+ }
+
+ private void restoreMatrix() {
+ Matrix pauseMatrix = (Matrix) mImageView.getTag(R.id.transition_image_transform);
+ if (pauseMatrix != null) {
+ ImageViewUtils.animateTransform(mImageView, pauseMatrix);
+ mImageView.setTag(R.id.transition_image_transform, null);
+ }
+ }
+
+ private void saveMatrix(Matrix pauseMatrix) {
+ mImageView.setTag(R.id.transition_image_transform, pauseMatrix);
+ ImageViewUtils.animateTransform(mImageView, mEndMatrix);
+ }
+ }
}
diff --git a/transition/transition/src/main/java/androidx/transition/ChangeScroll.java b/transition/transition/src/main/java/androidx/transition/ChangeScroll.java
index d2b9a21..03fa492 100644
--- a/transition/transition/src/main/java/androidx/transition/ChangeScroll.java
+++ b/transition/transition/src/main/java/androidx/transition/ChangeScroll.java
@@ -57,6 +57,11 @@
captureValues(transitionValues);
}
+ @Override
+ public boolean isSeekingSupported() {
+ return true;
+ }
+
@Nullable
@Override
public String[] getTransitionProperties() {
diff --git a/transition/transition/src/main/java/androidx/transition/ChangeTransform.java b/transition/transition/src/main/java/androidx/transition/ChangeTransform.java
index 84b869f..0d0cd09 100644
--- a/transition/transition/src/main/java/androidx/transition/ChangeTransform.java
+++ b/transition/transition/src/main/java/androidx/transition/ChangeTransform.java
@@ -326,48 +326,8 @@
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(pathAnimatorMatrix,
valuesProperty, translationProperty);
- final Matrix finalEndMatrix = endMatrix;
-
- AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
- private boolean mIsCanceled;
- private Matrix mTempMatrix = new Matrix();
-
- @Override
- public void onAnimationCancel(Animator animation) {
- mIsCanceled = true;
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- if (!mIsCanceled) {
- if (handleParentChange && mUseOverlay) {
- setCurrentMatrix(finalEndMatrix);
- } else {
- view.setTag(R.id.transition_transform, null);
- view.setTag(R.id.parent_matrix, null);
- }
- }
- ViewUtils.setAnimationMatrix(view, null);
- transforms.restore(view);
- }
-
- @Override
- public void onAnimationPause(Animator animation) {
- Matrix currentMatrix = pathAnimatorMatrix.getMatrix();
- setCurrentMatrix(currentMatrix);
- }
-
- @Override
- public void onAnimationResume(Animator animation) {
- setIdentityTransforms(view);
- }
-
- private void setCurrentMatrix(Matrix currentMatrix) {
- mTempMatrix.set(currentMatrix);
- view.setTag(R.id.transition_transform, mTempMatrix);
- transforms.restore(view);
- }
- };
+ Listener listener = new Listener(view, transforms, pathAnimatorMatrix, endMatrix,
+ handleParentChange, mUseOverlay);
animator.addListener(listener);
AnimatorUtils.addPauseListener(animator, listener);
@@ -591,4 +551,61 @@
}
}
+ private static class Listener extends AnimatorListenerAdapter implements
+ AnimatorUtils.AnimatorPauseListenerCompat {
+ private boolean mIsCanceled;
+ private final Matrix mTempMatrix = new Matrix();
+ private final boolean mHandleParentChange;
+ private final boolean mUseOverlay;
+ private final View mView;
+ private final Transforms mTransforms;
+ private final PathAnimatorMatrix mPathAnimatorMatrix;
+ private final Matrix mEndMatrix;
+
+ Listener(View view, Transforms transforms, PathAnimatorMatrix pathAnimatorMatrix,
+ Matrix endMatrix, boolean handleParentChange, boolean useOverlay) {
+ mHandleParentChange = handleParentChange;
+ mUseOverlay = useOverlay;
+ mView = view;
+ mTransforms = transforms;
+ mPathAnimatorMatrix = pathAnimatorMatrix;
+ mEndMatrix = endMatrix;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mIsCanceled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!mIsCanceled) {
+ if (mHandleParentChange && mUseOverlay) {
+ setCurrentMatrix(mEndMatrix);
+ } else {
+ mView.setTag(R.id.transition_transform, null);
+ mView.setTag(R.id.parent_matrix, null);
+ }
+ }
+ ViewUtils.setAnimationMatrix(mView, null);
+ mTransforms.restore(mView);
+ }
+
+ @Override
+ public void onAnimationPause(Animator animation) {
+ Matrix currentMatrix = mPathAnimatorMatrix.getMatrix();
+ setCurrentMatrix(currentMatrix);
+ }
+
+ @Override
+ public void onAnimationResume(Animator animation) {
+ setIdentityTransforms(mView);
+ }
+
+ private void setCurrentMatrix(Matrix currentMatrix) {
+ mTempMatrix.set(currentMatrix);
+ mView.setTag(R.id.transition_transform, mTempMatrix);
+ mTransforms.restore(mView);
+ }
+ }
}
diff --git a/transition/transition/src/main/java/androidx/transition/Explode.java b/transition/transition/src/main/java/androidx/transition/Explode.java
index ea53bc2..a671288 100644
--- a/transition/transition/src/main/java/androidx/transition/Explode.java
+++ b/transition/transition/src/main/java/androidx/transition/Explode.java
@@ -79,6 +79,11 @@
captureValues(transitionValues);
}
+ @Override
+ public boolean isSeekingSupported() {
+ return true;
+ }
+
@Nullable
@Override
public Animator onAppear(@NonNull ViewGroup sceneRoot, @NonNull View view,
diff --git a/transition/transition/src/main/java/androidx/transition/Fade.java b/transition/transition/src/main/java/androidx/transition/Fade.java
index ac1f499..a264655 100644
--- a/transition/transition/src/main/java/androidx/transition/Fade.java
+++ b/transition/transition/src/main/java/androidx/transition/Fade.java
@@ -118,6 +118,11 @@
ViewUtils.getTransitionAlpha(transitionValues.view));
}
+ @Override
+ public boolean isSeekingSupported() {
+ return true;
+ }
+
/**
* Utility method to handle creating and running the Animator.
*/
@@ -133,14 +138,6 @@
}
FadeAnimatorListener listener = new FadeAnimatorListener(view);
anim.addListener(listener);
- addListener(new TransitionListenerAdapter() {
- @Override
- public void onTransitionEnd(@NonNull Transition transition) {
- ViewUtils.setTransitionAlpha(view, 1);
- ViewUtils.clearNonTransitionAlpha(view);
- transition.removeListener(this);
- }
- });
return anim;
}
@@ -153,6 +150,7 @@
Log.d(LOG_TAG, "Fade.onAppear: startView, startVis, endView, endVis = "
+ startView + ", " + view);
}
+ ViewUtils.saveNonTransitionAlpha(view);
float startAlpha = getStartAlpha(startValues, 0);
if (startAlpha == 1) {
startAlpha = 0;
@@ -166,7 +164,11 @@
@Nullable TransitionValues startValues, @Nullable TransitionValues endValues) {
ViewUtils.saveNonTransitionAlpha(view);
float startAlpha = getStartAlpha(startValues, 1);
- return createAnimation(view, startAlpha, 0);
+ Animator animator = createAnimation(view, startAlpha, 0);
+ if (animator == null) {
+ ViewUtils.setTransitionAlpha(view, getStartAlpha(endValues, 1f));
+ }
+ return animator;
}
private static float getStartAlpha(TransitionValues startValues, float fallbackValue) {
@@ -201,11 +203,22 @@
@Override
public void onAnimationEnd(Animator animation) {
ViewUtils.setTransitionAlpha(mView, 1);
+ ViewUtils.clearNonTransitionAlpha(mView);
if (mLayerTypeChanged) {
mView.setLayerType(View.LAYER_TYPE_NONE, null);
}
}
+ @Override
+ public void onAnimationEnd(Animator animation, boolean isReverse) {
+ if (!isReverse) {
+ ViewUtils.setTransitionAlpha(mView, 1);
+ ViewUtils.clearNonTransitionAlpha(mView);
+ }
+ if (mLayerTypeChanged) {
+ mView.setLayerType(View.LAYER_TYPE_NONE, null);
+ }
+ }
}
}
diff --git a/transition/transition/src/main/java/androidx/transition/Slide.java b/transition/transition/src/main/java/androidx/transition/Slide.java
index ae54f98..62d2dcf 100644
--- a/transition/transition/src/main/java/androidx/transition/Slide.java
+++ b/transition/transition/src/main/java/androidx/transition/Slide.java
@@ -194,6 +194,11 @@
captureValues(transitionValues);
}
+ @Override
+ public boolean isSeekingSupported() {
+ return true;
+ }
+
/**
* Change the edge that Views appear and disappear from.
*
diff --git a/transition/transition/src/main/java/androidx/transition/Transition.java b/transition/transition/src/main/java/androidx/transition/Transition.java
index fc1a168..8c32506 100644
--- a/transition/transition/src/main/java/androidx/transition/Transition.java
+++ b/transition/transition/src/main/java/androidx/transition/Transition.java
@@ -20,13 +20,16 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.graphics.Path;
import android.graphics.Rect;
+import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
@@ -40,14 +43,19 @@
import android.widget.ListView;
import android.widget.Spinner;
+import androidx.annotation.DoNotInline;
import androidx.annotation.IdRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.collection.ArrayMap;
import androidx.collection.LongSparseArray;
import androidx.core.content.res.TypedArrayUtils;
+import androidx.core.os.BuildCompat;
+import androidx.core.util.Consumer;
import androidx.core.view.ViewCompat;
import java.lang.annotation.Retention;
@@ -200,6 +208,7 @@
private int[] mMatchOrder = DEFAULT_MATCH_ORDER;
private ArrayList<TransitionValues> mStartValuesList; // only valid after playTransition starts
private ArrayList<TransitionValues> mEndValuesList; // only valid after playTransitions starts
+ private TransitionListener[] mListenersCache;
// Per-animator information used for later canceling when future transitions overlap
private static ThreadLocal<ArrayMap<Animator, Transition.AnimationInfo>> sRunningAnimators =
@@ -220,21 +229,21 @@
// Number of per-target instances of this Transition currently running. This count is
// determined by calls to start() and end()
- private int mNumInstances = 0;
+ int mNumInstances = 0;
// Whether this transition is currently paused, due to a call to pause()
private boolean mPaused = false;
// Whether this transition has ended. Used to avoid pause/resume on transitions
// that have completed
- private boolean mEnded = false;
+ boolean mEnded = false;
// The set of listeners to be sent transition lifecycle events.
private ArrayList<Transition.TransitionListener> mListeners = null;
// The set of animators collected from calls to createAnimator(),
// to be run in runAnimators()
- private ArrayList<Animator> mAnimators = new ArrayList<>();
+ ArrayList<Animator> mAnimators = new ArrayList<>();
// The function for calculating the Animation start delay.
TransitionPropagation mPropagation;
@@ -251,6 +260,18 @@
// for adding curves to x/y View motion.
private PathMotion mPathMotion = STRAIGHT_PATH_MOTION;
+ // The total duration of this Transition, in milliseconds. This is used only if
+ // TransitionManager.controlDelayedTransition() is called to begin a seekable Transition.
+ long mTotalDuration;
+
+ // The SeekController created in TransitionManager.controlDelayedTransition() on the
+ // root TransitionSet.
+ SeekController mSeekController;
+
+ // For Transitions in a TransitionSet that are played sequentially, this is the offset
+ // (in milliseconds) from the start of the containing TransitionSet of this Transition
+ long mSeekOffsetInParent;
+
/**
* Constructs a Transition object with no target objects. A transition with
* no targets defaults to running on all target objects in the scene hierarchy
@@ -328,6 +349,19 @@
}
/**
+ * If this Transition is not part of a TransitionSet, this is returned. If it is part
+ * of a TransitionSet, the parent TransitionSets are walked until a TransitionSet is found
+ * that isn't contained in another TransitionSet.
+ */
+ @NonNull
+ public final Transition getRootTransition() {
+ if (mParent != null) {
+ return mParent.getRootTransition();
+ }
+ return this;
+ }
+
+ /**
* Sets the duration of this transition. By default, there is no duration
* (indicated by a negative number), which means that the Animator created by
* the transition will have its own specified duration. If the duration of a
@@ -487,6 +521,20 @@
}
/**
+ * Creates and returns a new TransitionSeekController, tied it to this Transition.
+ * This should only be called once on the cloned transition for controlling the
+ * Transition's progress. The Transition will begin without starting any of the
+ * animations.
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ @NonNull
+ TransitionSeekController createSeekController() {
+ mSeekController = new SeekController();
+ addListener(mSeekController);
+ return mSeekController;
+ }
+
+ /**
* Sets the order in which Transition matches View start and end values.
* <p>
* The default behavior is to match first by {@link android.view.View#getTransitionName()},
@@ -664,6 +712,7 @@
ArrayMap<View, TransitionValues> unmatchedStart = new ArrayMap<>(startValues.mViewValues);
ArrayMap<View, TransitionValues> unmatchedEnd = new ArrayMap<>(endValues.mViewValues);
+ //noinspection ForLoopReplaceableByForEach
for (int i = 0; i < mMatchOrder.length; i++) {
switch (mMatchOrder[i]) {
case MATCH_INSTANCE:
@@ -695,6 +744,7 @@
* TransitionSet subclass overrides this method and delegates it to
* each of its children in succession.
*/
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
void createAnimators(@NonNull ViewGroup sceneRoot, @NonNull TransitionValuesMaps startValues,
@NonNull TransitionValuesMaps endValues,
@NonNull ArrayList<TransitionValues> startValuesList,
@@ -706,6 +756,7 @@
long minStartDelay = Long.MAX_VALUE;
SparseIntArray startDelays = new SparseIntArray();
int startValuesListCount = startValuesList.size();
+ boolean hasSeekController = getRootTransition().mSeekController != null;
for (int i = 0; i < startValuesListCount; ++i) {
TransitionValues start = startValuesList.get(i);
TransitionValues end = endValuesList.get(i);
@@ -780,7 +831,12 @@
minStartDelay = Math.min(delay, minStartDelay);
}
AnimationInfo info = new AnimationInfo(view, getName(), this,
- ViewUtils.getWindowId(sceneRoot), infoValues);
+ ViewUtils.getWindowId(sceneRoot), infoValues, animator);
+ if (hasSeekController) {
+ AnimatorSet set = new AnimatorSet();
+ set.play(animator);
+ animator = set;
+ }
runningAnimators.put(animator, info);
mAnimators.add(animator);
}
@@ -791,8 +847,10 @@
for (int i = 0; i < startDelays.size(); i++) {
int index = startDelays.keyAt(i);
Animator animator = mAnimators.get(index);
- long delay = startDelays.valueAt(i) - minStartDelay + animator.getStartDelay();
- animator.setStartDelay(delay);
+ AnimationInfo info = runningAnimators.get(animator);
+ long delay = startDelays.valueAt(i) - minStartDelay
+ + info.mAnimator.getStartDelay();
+ info.mAnimator.setStartDelay(delay);
}
}
}
@@ -801,7 +859,7 @@
* Internal utility method for checking whether a given view/id
* is valid for this transition, where "valid" means that either
* the Transition has no target/targetId list (the default, in which
- * cause the transition should act on all views in the hiearchy), or
+ * cause the transition should act on all views in the hierarchy), or
* the given view is in the target list or the view id is in the
* targetId list. If the target parameter is null, then the target list
* is not checked (this is in the case of ListView items, where the
@@ -861,7 +919,7 @@
/**
* This is called internally once all animations have been set up by the
- * transition hierarchy. \
+ * transition hierarchy.
*
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -906,6 +964,42 @@
}
/**
+ * Configures the animators to be ready for animation.
+ *
+ * The animators' start delay, duration, and interpolator are set based on the Transition's
+ * values. The duration is calculated. It also adds the animators to mCurrentAnimators so that
+ * each animator can support seeking.
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ void prepareAnimatorsForSeeking() {
+ ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
+ // Now prepare every Animator that was previously created for this transition
+ mTotalDuration = 0;
+ for (int i = 0; i < mAnimators.size(); i++) {
+ Animator anim = mAnimators.get(i);
+ if (DBG) {
+ Log.d(LOG_TAG, " anim: " + anim);
+ }
+ AnimationInfo info = runningAnimators.get(anim);
+ if (anim != null && info != null) {
+ if (getDuration() >= 0) {
+ info.mAnimator.setDuration(getDuration());
+ }
+ if (getStartDelay() >= 0) {
+ info.mAnimator.setStartDelay(
+ getStartDelay() + info.mAnimator.getStartDelay());
+ }
+ if (getInterpolator() != null) {
+ info.mAnimator.setInterpolator(getInterpolator());
+ }
+ mCurrentAnimators.add(anim);
+ mTotalDuration = Math.max(mTotalDuration, Impl26.getTotalDuration(anim));
+ }
+ }
+ mAnimators.clear();
+ }
+
+ /**
* Captures the values in the start scene for the properties that this
* transition monitors. These values are then passed as the startValues
* structure in a later call to
@@ -1454,6 +1548,20 @@
}
/**
+ * Returns {@code true} if the Transition can be used by
+ * {@link TransitionManager#controlDelayedTransition(ViewGroup, Transition)}. This means
+ * that any the state must be ready before any {@link Animator} returned by
+ * {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)} has started and
+ * if the Animator has ended, it must be able to restore the state when starting in reverse.
+ * If a Transition must know when the entire transition has ended, a {@link TransitionListener}
+ * can be added to {@link #getRootTransition()} and it can listen for
+ * {@link TransitionListener#onTransitionEnd(Transition)}.
+ */
+ public boolean isSeekingSupported() {
+ return false;
+ }
+
+ /**
* Recursive method that captures values for the given view and the
* hierarchy underneath it.
*
@@ -1715,14 +1823,7 @@
Animator animator = mCurrentAnimators.get(i);
AnimatorUtils.pause(animator);
}
- if (mListeners != null && mListeners.size() > 0) {
- @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
- (ArrayList<TransitionListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onTransitionPause(this);
- }
- }
+ notifyListeners(TransitionNotification.ON_PAUSE, false);
mPaused = true;
}
}
@@ -1742,14 +1843,7 @@
Animator animator = mCurrentAnimators.get(i);
AnimatorUtils.resume(animator);
}
- if (mListeners != null && mListeners.size() > 0) {
- @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
- (ArrayList<TransitionListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onTransitionResume(this);
- }
- }
+ notifyListeners(TransitionNotification.ON_RESUME, false);
}
mPaused = false;
}
@@ -1760,6 +1854,7 @@
* createAnimators() to set things up and create all of the animations and then
* runAnimations() to actually start the animations.
*/
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
void playTransition(@NonNull ViewGroup sceneRoot) {
mStartValuesList = new ArrayList<>();
mEndValuesList = new ArrayList<>();
@@ -1784,7 +1879,22 @@
boolean cancel = (startValues != null || endValues != null)
&& oldInfo.mTransition.isTransitionRequired(oldValues, endValues);
if (cancel) {
- if (anim.isRunning() || anim.isStarted()) {
+ Transition transition = oldInfo.mTransition;
+ if (transition.getRootTransition().mSeekController != null) {
+ // Seeking, so cancel the transition directly rather than going through
+ // a listener
+ anim.cancel();
+ transition.mCurrentAnimators.remove(anim);
+ runningAnimators.remove(anim);
+ if (transition.mCurrentAnimators.size() == 0) {
+ transition.notifyListeners(TransitionNotification.ON_CANCEL, false);
+ if (!transition.mEnded) {
+ transition.mEnded = true;
+ transition.notifyListeners(TransitionNotification.ON_END,
+ false);
+ }
+ }
+ } else if (anim.isRunning() || anim.isStarted()) {
if (DBG) {
Log.d(LOG_TAG, "Canceling anim " + anim);
}
@@ -1801,7 +1911,13 @@
}
createAnimators(sceneRoot, mStartValues, mEndValues, mStartValuesList, mEndValuesList);
- runAnimators();
+ if (mSeekController == null) {
+ runAnimators();
+ } else if (BuildCompat.isAtLeastU()) {
+ prepareAnimatorsForSeeking();
+ mSeekController.setCurrentPlayTimeMillis(0);
+ mSeekController.ready();
+ }
}
/**
@@ -1909,14 +2025,7 @@
@RestrictTo(LIBRARY_GROUP_PREFIX)
protected void start() {
if (mNumInstances == 0) {
- if (mListeners != null && mListeners.size() > 0) {
- @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
- (ArrayList<TransitionListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onTransitionStart(this);
- }
- }
+ notifyListeners(TransitionNotification.ON_START, false);
mEnded = false;
}
mNumInstances++;
@@ -1936,14 +2045,7 @@
protected void end() {
--mNumInstances;
if (mNumInstances == 0) {
- if (mListeners != null && mListeners.size() > 0) {
- @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
- (ArrayList<TransitionListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onTransitionEnd(this);
- }
- }
+ notifyListeners(TransitionNotification.ON_END, false);
for (int i = 0; i < mStartValues.mItemIdValues.size(); ++i) {
View view = mStartValues.mItemIdValues.valueAt(i);
if (view != null) {
@@ -1996,14 +2098,7 @@
Animator animator = mCurrentAnimators.get(i);
animator.cancel();
}
- if (mListeners != null && mListeners.size() > 0) {
- @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
- (ArrayList<TransitionListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onTransitionCancel(this);
- }
- }
+ notifyListeners(TransitionNotification.ON_CANCEL, false);
}
/**
@@ -2169,6 +2264,7 @@
return;
}
boolean containsAll = true;
+ //noinspection ForLoopReplaceableByForEach
for (int i = 0; i < propertyNames.length; i++) {
if (!transitionValues.values.containsKey(propertyNames[i])) {
containsAll = false;
@@ -2201,6 +2297,10 @@
clone.mEndValues = new TransitionValuesMaps();
clone.mStartValuesList = null;
clone.mEndValuesList = null;
+ clone.mSeekController = null;
+ if (mListeners != null) {
+ clone.mListeners = new ArrayList<>(mListeners);
+ }
return clone;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
@@ -2224,39 +2324,114 @@
return mName;
}
+ /**
+ * Calls notification on each listener.
+ */
+ void notifyListeners(TransitionNotification notification, boolean isReversed) {
+ if (mListeners != null && !mListeners.isEmpty()) {
+ // Use a cache so that we don't have to keep allocating on every notification
+ int size = mListeners.size();
+ TransitionListener[] listeners = mListenersCache == null
+ ? new TransitionListener[size] : mListenersCache;
+ mListenersCache = null;
+ listeners = mListeners.toArray(listeners);
+ for (int i = 0; i < size; i++) {
+ notification.notifyListener(listeners[i], Transition.this, isReversed);
+ listeners[i] = null;
+ }
+ mListenersCache = listeners;
+ }
+ }
+
+ /**
+ * Returns the total duration of this Transition. This is only valid after the transition has
+ * been started.
+ */
+ final long getTotalDurationMillis() {
+ return mTotalDuration;
+ }
+
+ /**
+ * Seek the Transition to playTimeMillis.
+ *
+ * @param playTimeMillis The current time (in milliseconds) of the transition. If it is less
+ * than 0, the transition will be set to the beginning. If it is
+ * larger than getTotalDurationMillis(), it will be set to the end.
+ * @param lastPlayTimeMillis The previous play time that was set. This can be negative to
+ * indicate that the transition hasn't been played yet or larger
+ * than getTotalDurationMillis() to indicate that it is playing
+ * backwards.
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ void setCurrentPlayTimeMillis(long playTimeMillis, long lastPlayTimeMillis) {
+ long duration = getTotalDurationMillis();
+ boolean isReversed = playTimeMillis < lastPlayTimeMillis;
+ if ((lastPlayTimeMillis < 0 && playTimeMillis >= 0)
+ || (lastPlayTimeMillis > duration && playTimeMillis <= duration)) {
+ mEnded = false;
+ notifyListeners(TransitionNotification.ON_START, isReversed);
+ }
+ for (int i = 0; i < mCurrentAnimators.size(); i++) {
+ Animator animator = mCurrentAnimators.get(i);
+ long animDuration = Impl26.getTotalDuration(animator);
+ long playTime = Math.min(Math.max(0, playTimeMillis), animDuration);
+ Impl26.setCurrentPlayTime(animator, playTime);
+ }
+
+ if ((playTimeMillis > duration && lastPlayTimeMillis <= duration)
+ || (playTimeMillis < 0 && lastPlayTimeMillis >= 0)
+ ) {
+ if (playTimeMillis > duration) {
+ // Only mark it as finished after the end. Otherwise, it won't
+ // receive pause/resume calls.
+ mEnded = true;
+ }
+ notifyListeners(TransitionNotification.ON_END, isReversed);
+ }
+ }
+
String toString(String indent) {
- String result = indent + getClass().getSimpleName() + "@"
- + Integer.toHexString(hashCode()) + ": ";
+ StringBuilder result = new StringBuilder(indent)
+ .append(getClass().getSimpleName())
+ .append("@")
+ .append(Integer.toHexString(hashCode()))
+ .append(": ");
if (mDuration != -1) {
- result += "dur(" + mDuration + ") ";
+ result.append("dur(")
+ .append(mDuration)
+ .append(") ");
}
if (mStartDelay != -1) {
- result += "dly(" + mStartDelay + ") ";
+ result.append("dly(")
+ .append(mStartDelay)
+ .append(") ");
}
if (mInterpolator != null) {
- result += "interp(" + mInterpolator + ") ";
+ result.append("interp(")
+ .append(mInterpolator)
+ .append(") ");
}
if (mTargetIds.size() > 0 || mTargets.size() > 0) {
- result += "tgts(";
+ result.append("tgts(");
if (mTargetIds.size() > 0) {
for (int i = 0; i < mTargetIds.size(); ++i) {
if (i > 0) {
- result += ", ";
+ result.append(", ");
}
- result += mTargetIds.get(i);
+ result.append(mTargetIds.get(i));
}
}
if (mTargets.size() > 0) {
for (int i = 0; i < mTargets.size(); ++i) {
if (i > 0) {
- result += ", ";
+ result.append(", ");
}
- result += mTargets.get(i);
+ result.append(mTargets.get(i));
}
}
- result += ")";
+ result.append(")");
}
- return result;
+ return result.toString();
}
/**
@@ -2273,6 +2448,16 @@
void onTransitionStart(@NonNull Transition transition);
/**
+ * Notification about the start of the transition.
+ *
+ * @param transition The started transition.
+ * @param isReverse {@code true} when seeking the transition backwards from the end.
+ */
+ default void onTransitionStart(@NonNull Transition transition, boolean isReverse) {
+ onTransitionStart(transition);
+ }
+
+ /**
* Notification about the end of the transition. Canceled transitions
* will always notify listeners of both the cancellation and end
* events. That is, {@link #onTransitionEnd(Transition)} is always called,
@@ -2284,6 +2469,21 @@
void onTransitionEnd(@NonNull Transition transition);
/**
+ * Notification about the end of the transition. Canceled transitions
+ * will always notify listeners of both the cancellation and end
+ * events. That is, {@link #onTransitionEnd(Transition, boolean)} is always called,
+ * regardless of whether the transition was canceled or played
+ * through to completion. Canceled transitions will have {@code isReverse}
+ * set to {@code false}.
+ *
+ * @param transition The transition which reached its end.
+ * @param isReverse {@code true} when seeking the transition backwards past the start.
+ */
+ default void onTransitionEnd(@NonNull Transition transition, boolean isReverse) {
+ onTransitionEnd(transition);
+ }
+
+ /**
* Notification about the cancellation of the transition.
* Note that cancel may be called by a parent {@link TransitionSet} on
* a child transition which has not yet started. This allows the child
@@ -2338,13 +2538,16 @@
Transition mTransition;
+ Animator mAnimator;
+
AnimationInfo(View view, String name, Transition transition, WindowIdImpl windowId,
- TransitionValues values) {
+ TransitionValues values, Animator animator) {
mView = view;
mName = name;
mValues = values;
mWindowId = windowId;
mTransition = transition;
+ mAnimator = animator;
}
}
@@ -2420,4 +2623,212 @@
public abstract Rect onGetEpicenter(@NonNull Transition transition);
}
+ /**
+ * Used internally by notifyListener() to call TransitionListener methods for this transition.
+ */
+ interface TransitionNotification {
+ /**
+ * Make a call on a TransitionListener
+ * @param listener The listener that this should call on.
+ * @param transition The Transition making the call.
+ */
+ void notifyListener(
+ @NonNull TransitionListener listener,
+ @NonNull Transition transition,
+ boolean isReversed
+ );
+
+ /**
+ * Call for TransitionListener#onTransitionStart()
+ */
+ TransitionNotification ON_START = TransitionListener::onTransitionStart;
+
+ /**
+ * Call for TransitionListener#onTransitionEnd()
+ */
+ TransitionNotification ON_END = TransitionListener::onTransitionEnd;
+
+ /**
+ * Call for TransitionListener#onTransitionCancel()
+ */
+ TransitionNotification ON_CANCEL =
+ (listener, transition, isReversed) -> listener.onTransitionCancel(transition);
+
+ /**
+ * Call for TransitionListener#onTransitionPause()
+ */
+ TransitionNotification ON_PAUSE =
+ (listener, transition, isReversed) -> listener.onTransitionPause(transition);
+
+ /**
+ * Call for TransitionListener#onTransitionResume()
+ */
+ TransitionNotification ON_RESUME =
+ (listener, transition, isReversed) -> listener.onTransitionResume(transition);
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private static class Impl26 {
+ @DoNotInline
+ static long getTotalDuration(Animator animator) {
+ return animator.getTotalDuration();
+ }
+
+ @DoNotInline
+ static void setCurrentPlayTime(Animator animator, long playTimeMillis) {
+ ((AnimatorSet) animator).setCurrentPlayTime(playTimeMillis);
+ }
+ }
+
+ /**
+ * Internal implementation of TransitionSeekController.
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ class SeekController extends TransitionListenerAdapter implements TransitionSeekController,
+ ValueAnimator.AnimatorUpdateListener {
+ private long mCurrentPlayTime = -1;
+ private ArrayList<Consumer<TransitionSeekController>> mOnReadyListeners = null;
+ private boolean mIsReady;
+ private boolean mIsCanceled;
+
+ private ValueAnimator mAnimator;
+ private boolean mIsAnimatingReversed;
+
+ @Override
+ public long getDurationMillis() {
+ return Transition.this.getTotalDurationMillis();
+ }
+
+ @Override
+ public long getCurrentPlayTimeMillis() {
+ return Math.min(getDurationMillis(), Math.max(0, mCurrentPlayTime));
+ }
+
+ @Override
+ public boolean isReady() {
+ return mIsReady;
+ }
+
+ public void ready() {
+ mIsReady = true;
+ if (mOnReadyListeners != null) {
+ ArrayList<Consumer<TransitionSeekController>> onReadyListeners = mOnReadyListeners;
+ mOnReadyListeners = null;
+ for (int i = 0; i < onReadyListeners.size(); i++) {
+ onReadyListeners.get(i).accept(this);
+ }
+ }
+ }
+
+ @Override
+ public void setCurrentPlayTimeMillis(long playTimeMillis) {
+ if (mAnimator != null) {
+ throw new IllegalStateException("setCurrentPlayTimeMillis() called after animation "
+ + "has been started");
+ }
+ if (playTimeMillis == mCurrentPlayTime) {
+ return; // no change
+ }
+
+ if (!mIsCanceled) {
+ if (playTimeMillis == 0 && mCurrentPlayTime > 0) {
+ // Force the transition to end
+ playTimeMillis = -1;
+ } else {
+ long duration = getDurationMillis();
+ // Force the transition to the end
+ if (playTimeMillis == duration && mCurrentPlayTime < duration) {
+ playTimeMillis = duration + 1;
+ }
+ }
+ if (playTimeMillis != mCurrentPlayTime) {
+ Transition.this.setCurrentPlayTimeMillis(playTimeMillis, mCurrentPlayTime);
+ mCurrentPlayTime = playTimeMillis;
+ }
+ }
+ }
+
+ @Override
+ public void addOnReadyListener(
+ @NonNull Consumer<TransitionSeekController> onReadyListener
+ ) {
+ if (isReady()) {
+ onReadyListener.accept(this);
+ return;
+ }
+ if (mOnReadyListeners == null) {
+ mOnReadyListeners = new ArrayList<>();
+ }
+ mOnReadyListeners.add(onReadyListener);
+ }
+
+ @Override
+ public void removeOnReadyListener(
+ @NonNull Consumer<TransitionSeekController> onReadyListener
+ ) {
+ if (mOnReadyListeners != null) {
+ mOnReadyListeners.remove(onReadyListener);
+ if (mOnReadyListeners.isEmpty()) {
+ mOnReadyListeners = null;
+ }
+ }
+ }
+
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ mIsCanceled = true;
+ }
+
+ @Override
+ public void onAnimationUpdate(@NonNull ValueAnimator valueAnimator) {
+ long time = Math.max(-1,
+ Math.min(getDurationMillis() + 1, mAnimator.getCurrentPlayTime())
+ );
+ if (mIsAnimatingReversed) {
+ time = getDurationMillis() - time;
+ }
+ Transition.this.setCurrentPlayTimeMillis(time, mCurrentPlayTime);
+ mCurrentPlayTime = time;
+ }
+
+ private void createAnimator() {
+ long duration = getDurationMillis() + 1;
+ mAnimator = ValueAnimator.ofInt((int) duration);
+ mAnimator.setInterpolator(null);
+ mAnimator.setDuration(duration);
+ mAnimator.addUpdateListener(this);
+ }
+
+ @Override
+ public void animateToEnd() {
+ if (mAnimator != null) {
+ mAnimator.cancel();
+ }
+ final long duration = getDurationMillis();
+ if (mCurrentPlayTime > duration) {
+ return; // we're already at the end
+ }
+ createAnimator();
+ mIsAnimatingReversed = false;
+ mAnimator.setCurrentPlayTime(mCurrentPlayTime);
+ mAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ notifyListeners(TransitionNotification.ON_END, false);
+ }
+ });
+ mAnimator.start();
+ }
+
+ @Override
+ public void animateToStart() {
+ if (mAnimator != null) {
+ mAnimator.cancel();
+ }
+ createAnimator();
+ mAnimator.setCurrentPlayTime(getDurationMillis() - mCurrentPlayTime);
+ mIsAnimatingReversed = true;
+ mAnimator.start();
+ }
+ }
}
diff --git a/transition/transition/src/main/java/androidx/transition/TransitionManager.java b/transition/transition/src/main/java/androidx/transition/TransitionManager.java
index ccfd27b..ea86ef7 100644
--- a/transition/transition/src/main/java/androidx/transition/TransitionManager.java
+++ b/transition/transition/src/main/java/androidx/transition/TransitionManager.java
@@ -24,7 +24,10 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.annotation.VisibleForTesting;
import androidx.collection.ArrayMap;
+import androidx.core.os.BuildCompat;
import androidx.core.view.ViewCompat;
import java.lang.ref.WeakReference;
@@ -193,6 +196,7 @@
}
}
+ @VisibleForTesting
static ArrayMap<ViewGroup, ArrayList<Transition>> getRunningTransitions() {
WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>> runningTransitions =
sRunningTransitions.get();
@@ -419,6 +423,60 @@
}
/**
+ * Create a {@link TransitionSeekController} to allow seeking an animation to a new
+ * scene defined by all changes within the given scene root between calling this method and
+ * the next rendered frame. Calling this method causes TransitionManager to capture current
+ * values in the scene root and then post a request to run a transition on the next frame.
+ * At that time, the new values in the scene root will be captured and changes
+ * will be animated. There is no need to create a Scene; it is implied by
+ * changes which take place between calling this method and the next frame when
+ * the transition begins.
+ *
+ * <p>Calling this method several times before the next frame (for example, if
+ * unrelated code also wants to make dynamic changes and run a transition on
+ * the same scene root), only the first call will trigger capturing values
+ * and exiting the current scene. Subsequent calls to the method with the
+ * same scene root during the same frame will be ignored.</p>
+ *
+ * @param sceneRoot The root of the View hierarchy to run the transition on.
+ * @param transition The transition to use for this change.
+ * @return a {@link TransitionSeekController} that can be used control the animation to the
+ * destination scene. {@code null} is returned when seeking is not supported on the scene,
+ * either because it is running on {@link android.os.Build.VERSION_CODES.TIRAMISU} or earlier,
+ * another Transition is being captured for {@code sceneRoot}, or {@code sceneRoot} hasn't
+ * had a layout yet.
+ * @throws IllegalArgumentException if {@code transition} returns {@code false} from
+ * {@link Transition#isSeekingSupported()}.
+ */
+ @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+ @Nullable
+ public static TransitionSeekController controlDelayedTransition(
+ @NonNull final ViewGroup sceneRoot,
+ @NonNull Transition transition
+ ) {
+ if (sPendingTransitions.contains(sceneRoot) || !ViewCompat.isLaidOut(sceneRoot)
+ || !BuildCompat.isAtLeastU()) {
+ return null;
+ }
+ if (!transition.isSeekingSupported()) {
+ throw new IllegalArgumentException("The Transition must support seeking.");
+ }
+ if (Transition.DBG) {
+ Log.d(LOG_TAG, "controlDelayedTransition: root, transition = "
+ + sceneRoot + ", " + transition);
+ }
+ sPendingTransitions.add(sceneRoot);
+ final Transition transitionClone = transition.clone();
+ final TransitionSet set = new TransitionSet();
+ set.addTransition(transitionClone);
+ sceneChangeSetup(sceneRoot, set);
+ Scene.setCurrentScene(sceneRoot, null);
+ sceneChangeRunTransition(sceneRoot, set);
+ sceneRoot.invalidate();
+ return set.createSeekController();
+ }
+
+ /**
* Ends all pending and ongoing transitions on the specified scene root.
*
* @param sceneRoot The root of the View hierarchy to end transitions on.
@@ -435,5 +493,4 @@
}
}
}
-
}
diff --git a/transition/transition/src/main/java/androidx/transition/TransitionSeekController.java b/transition/transition/src/main/java/androidx/transition/TransitionSeekController.java
new file mode 100644
index 0000000..bcede11
--- /dev/null
+++ b/transition/transition/src/main/java/androidx/transition/TransitionSeekController.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.transition;
+
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+
+/**
+ * Returned from {@link TransitionManager#controlDelayedTransition(ViewGroup, Transition)}
+ * to allow manually controlling the animations within a Transition using
+ * {@link #setCurrentPlayTimeMillis(long)}. The transition will be ready to seek when
+ * {@link #isReady()} is {@code true}.
+ */
+public interface TransitionSeekController {
+ /**
+ * @return The total duration, in milliseconds, of the Transition's animations.
+ */
+ long getDurationMillis();
+
+ /**
+ * @return The time, in milliseconds, of the animation. This will be between 0
+ * and {@link #getDurationMillis()}.
+ */
+ long getCurrentPlayTimeMillis();
+
+ /**
+ * Returns {@code true} when the Transition is ready to seek or {@code false}
+ * when the Transition's animations have yet to be built.
+ */
+ boolean isReady();
+
+ /**
+ * Runs the animation backwards toward the start. {@link #setCurrentPlayTimeMillis(long)}
+ * will not be allowed after executing this. When the animation completes,
+ * {@link androidx.transition.Transition.TransitionListener#onTransitionEnd(Transition)}
+ * will be called with the {@code isReverse} parameter {@code true}.
+ *
+ * The developer will likely want to run
+ * {@link TransitionManager#beginDelayedTransition(ViewGroup, Transition)} to set the state
+ * back to the beginning state after it ends.
+ *
+ * After calling this, {@link #setCurrentPlayTimeMillis(long)} may not be called.
+ */
+ void animateToStart();
+
+ /**
+ * Runs the animation forwards toward the end. {@link #setCurrentPlayTimeMillis(long)}
+ * will not be allowed after executing this. When the animation completes,
+ * {@link androidx.transition.Transition.TransitionListener#onTransitionEnd(Transition)}
+ * will be called with the {@code isReverse} parameter {@code false}.
+ *
+ * After the Transition ends, the state will reach the final state set after
+ * {@link TransitionManager#controlDelayedTransition(ViewGroup, Transition)}.
+ *
+ * After calling this, {@link #setCurrentPlayTimeMillis(long)} may not be called.
+ */
+ void animateToEnd();
+
+ /**
+ * Sets the position of the Transition's animation. {@code playTimeMillis} should be
+ * between 0 and {@link #getDurationMillis()}. This should not be called when
+ * {@link #isReady()} is {@code false}.
+ *
+ * @param playTimeMillis The time, between 0 and {@link #getDurationMillis()} that the
+ * animation should play.
+ */
+ void setCurrentPlayTimeMillis(long playTimeMillis);
+
+ /**
+ * Adds a listener to know when {@link #isReady()} is {@code true}. The listener will
+ * be removed once notified as {@link #isReady()} can only be made true once. If
+ * {@link #isReady()} is already {@code true}, then it will be notified immediately.
+ *
+ * @param onReadyListener The listener to be notified when the Transition is ready.
+ */
+ void addOnReadyListener(@NonNull Consumer<TransitionSeekController> onReadyListener);
+
+ /**
+ * Removes {@code onReadyListener} that was previously added in
+ * {@link #addOnReadyListener(Consumer)} so that it won't be called.
+ *
+ * @param onReadyListener The listener to be removed so that it won't be notified when ready.
+ */
+ void removeOnReadyListener(@NonNull Consumer<TransitionSeekController> onReadyListener);
+}
+
diff --git a/transition/transition/src/main/java/androidx/transition/TransitionSet.java b/transition/transition/src/main/java/androidx/transition/TransitionSet.java
index b1a1a3e..c875b2b 100644
--- a/transition/transition/src/main/java/androidx/transition/TransitionSet.java
+++ b/transition/transition/src/main/java/androidx/transition/TransitionSet.java
@@ -23,6 +23,7 @@
import android.content.Context;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
+import android.os.Build;
import android.util.AndroidRuntimeException;
import android.util.AttributeSet;
import android.view.View;
@@ -31,6 +32,7 @@
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.content.res.TypedArrayUtils;
@@ -77,7 +79,7 @@
*/
private static final int FLAG_CHANGE_EPICENTER = 0x08;
- private ArrayList<Transition> mTransitions = new ArrayList<>();
+ ArrayList<Transition> mTransitions = new ArrayList<>();
private boolean mPlayTogether = true;
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mCurrentListeners;
@@ -515,6 +517,124 @@
}
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ @Override
+ void prepareAnimatorsForSeeking() {
+ mTotalDuration = 0;
+ TransitionListenerAdapter listener = new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ mTransitions.remove(transition);
+ if (mTransitions.isEmpty()) {
+ notifyListeners(TransitionNotification.ON_CANCEL, false);
+ if (!mEnded) {
+ mEnded = true;
+ notifyListeners(TransitionNotification.ON_END, false);
+ }
+ }
+ }
+ };
+ for (int i = 0; i < mTransitions.size(); ++i) {
+ Transition transition = mTransitions.get(i);
+ transition.addListener(listener);
+ transition.prepareAnimatorsForSeeking();
+ long duration = transition.getTotalDurationMillis();
+ if (mPlayTogether) {
+ mTotalDuration = Math.max(mTotalDuration, duration);
+ } else {
+ transition.mSeekOffsetInParent = mTotalDuration;
+ mTotalDuration += duration;
+ }
+ }
+ }
+
+ /**
+ * Returns the index of the Transition that is playing at playTime. If no such transition
+ * exists, either because that Transition has been canceled or the TransitionSet is empty,
+ * the index of the one prior, or 0 will be returned.
+ */
+ private int indexOfTransition(long playTime) {
+ for (int i = 1; i < mTransitions.size(); i++) {
+ Transition transition = mTransitions.get(i);
+ if (transition.mSeekOffsetInParent > playTime) {
+ return i - 1;
+ }
+ }
+ return mTransitions.size() - 1;
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ @Override
+ void setCurrentPlayTimeMillis(long playTimeMillis, long lastPlayTimeMillis) {
+ long duration = getTotalDurationMillis();
+ if (mParent != null && ((playTimeMillis < 0 && lastPlayTimeMillis < 0)
+ || (playTimeMillis > duration && lastPlayTimeMillis > duration))
+ ) {
+ return;
+ }
+ boolean isReverse = playTimeMillis < lastPlayTimeMillis;
+ if ((playTimeMillis >= 0 && lastPlayTimeMillis < 0)
+ || (playTimeMillis <= duration && lastPlayTimeMillis > duration)
+ ) {
+ mEnded = false;
+ notifyListeners(TransitionNotification.ON_START, isReverse);
+ }
+ if (mPlayTogether) {
+ for (int i = 0; i < mTransitions.size(); i++) {
+ Transition transition = mTransitions.get(i);
+ transition.setCurrentPlayTimeMillis(playTimeMillis, lastPlayTimeMillis);
+ }
+ } else {
+ // find the Transition that lastPlayTimeMillis was using
+ int startIndex = indexOfTransition(lastPlayTimeMillis);
+
+ if (playTimeMillis >= lastPlayTimeMillis) {
+ // move forward through transitions
+ for (int i = startIndex; i < mTransitions.size(); i++) {
+ Transition transition = mTransitions.get(i);
+ long transitionStart = transition.mSeekOffsetInParent;
+ long playTime = playTimeMillis - transitionStart;
+ if (playTime < 0) {
+ break; // went past
+ }
+ long lastPlayTime = lastPlayTimeMillis - transitionStart;
+ transition.setCurrentPlayTimeMillis(playTime, lastPlayTime);
+ }
+ } else {
+ // move backwards through transitions
+ for (int i = startIndex; i >= 0; i--) {
+ Transition transition = mTransitions.get(i);
+ long transitionStart = transition.mSeekOffsetInParent;
+ long playTime = playTimeMillis - transitionStart;
+ long lastPlayTime = lastPlayTimeMillis - transitionStart;
+ transition.setCurrentPlayTimeMillis(playTime, lastPlayTime);
+ if (playTime >= 0) {
+ break;
+ }
+ }
+ }
+ }
+ if (mParent != null && ((playTimeMillis > duration && lastPlayTimeMillis <= duration)
+ || (playTimeMillis < 0 && lastPlayTimeMillis >= 0))
+ ) {
+ if (playTimeMillis > duration) {
+ mEnded = true;
+ }
+ notifyListeners(TransitionNotification.ON_END, isReverse);
+ }
+ }
+
+ @Override
+ public boolean isSeekingSupported() {
+ int numTransitions = mTransitions.size();
+ for (int i = 0; i < numTransitions; i++) {
+ if (!mTransitions.get(i).isSeekingSupported()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
@Override
public void captureStartValues(@NonNull TransitionValues transitionValues) {
if (isValidTarget(transitionValues.view)) {
diff --git a/transition/transition/src/main/java/androidx/transition/TranslationAnimationCreator.java b/transition/transition/src/main/java/androidx/transition/TranslationAnimationCreator.java
index 7a24a33..114674c 100644
--- a/transition/transition/src/main/java/androidx/transition/TranslationAnimationCreator.java
+++ b/transition/transition/src/main/java/androidx/transition/TranslationAnimationCreator.java
@@ -77,7 +77,6 @@
startPosX, startPosY, terminalX, terminalY);
transition.addListener(listener);
anim.addListener(listener);
- AnimatorUtils.addPauseListener(anim, listener);
anim.setInterpolator(interpolator);
return anim;
}
@@ -87,20 +86,17 @@
private final View mViewInHierarchy;
private final View mMovingView;
- private final int mStartX;
- private final int mStartY;
private int[] mTransitionPosition;
private float mPausedX;
private float mPausedY;
private final float mTerminalX;
private final float mTerminalY;
+ private boolean mIsAnimationCancelCalled;
TransitionPositionListener(View movingView, View viewInHierarchy,
int startX, int startY, float terminalX, float terminalY) {
mMovingView = movingView;
mViewInHierarchy = viewInHierarchy;
- mStartX = startX - Math.round(mMovingView.getTranslationX());
- mStartY = startY - Math.round(mMovingView.getTranslationY());
mTerminalX = terminalX;
mTerminalY = terminalY;
mTransitionPosition = (int[]) mViewInHierarchy.getTag(R.id.transition_position);
@@ -111,26 +107,8 @@
@Override
public void onAnimationCancel(Animator animation) {
- if (mTransitionPosition == null) {
- mTransitionPosition = new int[2];
- }
- mTransitionPosition[0] = Math.round(mStartX + mMovingView.getTranslationX());
- mTransitionPosition[1] = Math.round(mStartY + mMovingView.getTranslationY());
- mViewInHierarchy.setTag(R.id.transition_position, mTransitionPosition);
- }
-
- @Override
- public void onAnimationPause(Animator animator) {
- mPausedX = mMovingView.getTranslationX();
- mPausedY = mMovingView.getTranslationY();
- mMovingView.setTranslationX(mTerminalX);
- mMovingView.setTranslationY(mTerminalY);
- }
-
- @Override
- public void onAnimationResume(Animator animator) {
- mMovingView.setTranslationX(mPausedX);
- mMovingView.setTranslationY(mPausedY);
+ setInterruptedPosition();
+ mIsAnimationCancelCalled = true;
}
@Override
@@ -138,22 +116,48 @@
}
@Override
+ public void onTransitionEnd(@NonNull Transition transition, boolean isReverse) {
+ if (!isReverse) {
+ mMovingView.setTranslationX(mTerminalX);
+ mMovingView.setTranslationY(mTerminalY);
+ }
+ }
+
+ @Override
public void onTransitionEnd(@NonNull Transition transition) {
- mMovingView.setTranslationX(mTerminalX);
- mMovingView.setTranslationY(mTerminalY);
- transition.removeListener(this);
}
@Override
public void onTransitionCancel(@NonNull Transition transition) {
+ if (!mIsAnimationCancelCalled) {
+ setInterruptedPosition();
+ }
+ mMovingView.setTranslationX(mTerminalX);
+ mMovingView.setTranslationY(mTerminalY);
+ int[] pos = new int[2];
+ mMovingView.getLocationOnScreen(pos);
}
@Override
public void onTransitionPause(@NonNull Transition transition) {
+ mPausedX = mMovingView.getTranslationX();
+ mPausedY = mMovingView.getTranslationY();
+ mMovingView.setTranslationX(mTerminalX);
+ mMovingView.setTranslationY(mTerminalY);
}
@Override
public void onTransitionResume(@NonNull Transition transition) {
+ mMovingView.setTranslationX(mPausedX);
+ mMovingView.setTranslationY(mPausedY);
+ }
+
+ private void setInterruptedPosition() {
+ if (mTransitionPosition == null) {
+ mTransitionPosition = new int[2];
+ }
+ mMovingView.getLocationOnScreen(mTransitionPosition);
+ mViewInHierarchy.setTag(R.id.transition_position, mTransitionPosition);
}
}
diff --git a/transition/transition/src/main/java/androidx/transition/Visibility.java b/transition/transition/src/main/java/androidx/transition/Visibility.java
index a9f85bd..8b3fc2e 100644
--- a/transition/transition/src/main/java/androidx/transition/Visibility.java
+++ b/transition/transition/src/main/java/androidx/transition/Visibility.java
@@ -436,31 +436,13 @@
ViewGroupUtils.getOverlay(sceneRoot).remove(overlayView);
} else {
startView.setTag(R.id.save_overlay_view, overlayView);
- final View finalOverlayView = overlayView;
- final ViewGroup overlayHost = sceneRoot;
- addListener(new TransitionListenerAdapter() {
- @Override
- public void onTransitionPause(@NonNull Transition transition) {
- ViewGroupUtils.getOverlay(overlayHost).remove(finalOverlayView);
- }
+ OverlayListener listener = new OverlayListener(sceneRoot, overlayView,
+ startView);
- @Override
- public void onTransitionResume(@NonNull Transition transition) {
- if (finalOverlayView.getParent() == null) {
- ViewGroupUtils.getOverlay(overlayHost).add(finalOverlayView);
- } else {
- cancel();
- }
- }
-
- @Override
- public void onTransitionEnd(@NonNull Transition transition) {
- startView.setTag(R.id.save_overlay_view, null);
- ViewGroupUtils.getOverlay(overlayHost).remove(finalOverlayView);
- transition.removeListener(this);
- }
- });
+ animator.addListener(listener);
+ AnimatorUtils.addPauseListener(animator, listener);
+ getRootTransition().addListener(listener);
}
}
return animator;
@@ -474,8 +456,7 @@
DisappearListener disappearListener = new DisappearListener(viewToKeep,
endVisibility, true);
animator.addListener(disappearListener);
- AnimatorUtils.addPauseListener(animator, disappearListener);
- addListener(disappearListener);
+ getRootTransition().addListener(disappearListener);
} else {
ViewUtils.setTransitionVisibility(viewToKeep, originalVisibility);
}
@@ -525,7 +506,7 @@
}
private static class DisappearListener extends AnimatorListenerAdapter
- implements TransitionListener, AnimatorUtils.AnimatorPauseListenerCompat {
+ implements TransitionListener {
private final View mView;
private final int mFinalVisibility;
@@ -544,24 +525,6 @@
suppressLayout(true);
}
- // This overrides both AnimatorListenerAdapter and
- // AnimatorUtilsApi14.AnimatorPauseListenerCompat
- @Override
- public void onAnimationPause(Animator animation) {
- if (!mCanceled) {
- ViewUtils.setTransitionVisibility(mView, mFinalVisibility);
- }
- }
-
- // This overrides both AnimatorListenerAdapter and
- // AnimatorUtilsApi14.AnimatorPauseListenerCompat
- @Override
- public void onAnimationResume(Animator animation) {
- if (!mCanceled) {
- ViewUtils.setTransitionVisibility(mView, View.VISIBLE);
- }
- }
-
@Override
public void onAnimationCancel(Animator animation) {
mCanceled = true;
@@ -581,13 +544,29 @@
}
@Override
+ public void onAnimationStart(@NonNull Animator animation, boolean isReverse) {
+ if (isReverse) {
+ ViewUtils.setTransitionVisibility(mView, View.VISIBLE);
+ if (mParent != null) {
+ mParent.invalidate();
+ }
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) {
+ if (!isReverse) {
+ hideViewWhenNotCanceled();
+ }
+ }
+
+ @Override
public void onTransitionStart(@NonNull Transition transition) {
// Do nothing
}
@Override
public void onTransitionEnd(@NonNull Transition transition) {
- hideViewWhenNotCanceled();
transition.removeListener(this);
}
@@ -598,11 +577,17 @@
@Override
public void onTransitionPause(@NonNull Transition transition) {
suppressLayout(false);
+ if (!mCanceled) {
+ ViewUtils.setTransitionVisibility(mView, mFinalVisibility);
+ }
}
@Override
public void onTransitionResume(@NonNull Transition transition) {
suppressLayout(true);
+ if (!mCanceled) {
+ ViewUtils.setTransitionVisibility(mView, View.VISIBLE);
+ }
}
private void hideViewWhenNotCanceled() {
@@ -624,4 +609,83 @@
}
}
}
+
+ private class OverlayListener extends AnimatorListenerAdapter implements TransitionListener,
+ AnimatorUtils.AnimatorPauseListenerCompat {
+ private final ViewGroup mOverlayHost;
+ private final View mOverlayView;
+ private final View mStartView;
+ private boolean mHasOverlay = true;
+
+ OverlayListener(ViewGroup overlayHost, View overlayView, View startView) {
+ mOverlayHost = overlayHost;
+ mOverlayView = overlayView;
+ mStartView = startView;
+ }
+
+ @Override
+ public void onAnimationPause(Animator animation) {
+ ViewGroupUtils.getOverlay(mOverlayHost).remove(mOverlayView);
+ }
+
+ @Override
+ public void onAnimationResume(Animator animation) {
+ if (mOverlayView.getParent() == null) {
+ ViewGroupUtils.getOverlay(mOverlayHost).add(mOverlayView);
+ } else {
+ cancel();
+ }
+ }
+
+ @Override
+ public void onAnimationStart(@NonNull Animator animation, boolean isReverse) {
+ if (isReverse) {
+ mStartView.setTag(R.id.save_overlay_view, mOverlayView);
+ ViewGroupUtils.getOverlay(mOverlayHost).add(mOverlayView);
+ mHasOverlay = true;
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ removeFromOverlay();
+ }
+
+ @Override
+ public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) {
+ if (!isReverse) {
+ removeFromOverlay();
+ }
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ transition.removeListener(this);
+ }
+
+ @Override
+ public void onTransitionStart(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ if (mHasOverlay) {
+ removeFromOverlay();
+ }
+ }
+
+ private void removeFromOverlay() {
+ mStartView.setTag(R.id.save_overlay_view, null);
+ ViewGroupUtils.getOverlay(mOverlayHost).remove(mOverlayView);
+ mHasOverlay = false;
+ }
+ }
}
diff --git a/transition/transition/src/main/res/values/ids.xml b/transition/transition/src/main/res/values/ids.xml
index b9dd584..c2f6195 100644
--- a/transition/transition/src/main/res/values/ids.xml
+++ b/transition/transition/src/main/res/values/ids.xml
@@ -20,6 +20,8 @@
<item name="transition_layout_save" type="id"/>
<item name="transition_position" type="id"/>
<item name="transition_transform" type="id"/>
+ <item name="transition_image_transform" type="id"/>
+ <item name="transition_clip" type="id"/>
<item name="parent_matrix" type="id"/>
<item name="ghost_view" type="id"/>
<item name="ghost_view_holder" type="id"/>
diff --git a/tv/tv-foundation/build.gradle b/tv/tv-foundation/build.gradle
index a53a44ee..eace93c 100644
--- a/tv/tv-foundation/build.gradle
+++ b/tv/tv-foundation/build.gradle
@@ -67,7 +67,7 @@
}
androidx {
- name = "androidx.tv:tv-foundation"
+ name = "TV Foundation"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2022"
description = "This library makes it easier for developers" +
diff --git a/tv/tv-material/build.gradle b/tv/tv-material/build.gradle
index 334a3f1..760e574 100644
--- a/tv/tv-material/build.gradle
+++ b/tv/tv-material/build.gradle
@@ -57,7 +57,7 @@
}
androidx {
- name = "androidx.tv:tv-material"
+ name = "TV Material"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2022"
description = "build TV applications using controls that adhere to Material Design Language."
diff --git a/tvprovider/tvprovider/build.gradle b/tvprovider/tvprovider/build.gradle
index 3ca9e91..4c19140 100644
--- a/tvprovider/tvprovider/build.gradle
+++ b/tvprovider/tvprovider/build.gradle
@@ -24,7 +24,7 @@
}
androidx {
- name = "Android Support TV Provider"
+ name = "TV Provider"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Support Library for TV Provider"
diff --git a/vectordrawable/vectordrawable-animated/build.gradle b/vectordrawable/vectordrawable-animated/build.gradle
index 2217ba2..8fd4fa3 100644
--- a/vectordrawable/vectordrawable-animated/build.gradle
+++ b/vectordrawable/vectordrawable-animated/build.gradle
@@ -34,7 +34,7 @@
}
androidx {
- name = "Android Support AnimatedVectorDrawable"
+ name = "AnimatedVectorDrawable"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.VECTORDRAWABLE_ANIMATED
inceptionYear = "2015"
diff --git a/vectordrawable/vectordrawable-seekable/build.gradle b/vectordrawable/vectordrawable-seekable/build.gradle
index aac1763..fa14340 100644
--- a/vectordrawable/vectordrawable-seekable/build.gradle
+++ b/vectordrawable/vectordrawable-seekable/build.gradle
@@ -36,7 +36,7 @@
}
androidx {
- name = "Android SeekableAnimatedVectorDrawable"
+ name = "SeekableAnimatedVectorDrawable"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.VECTORDRAWABLE_SEEKABLE
inceptionYear = "2020"
diff --git a/vectordrawable/vectordrawable/build.gradle b/vectordrawable/vectordrawable/build.gradle
index 9a818af..1d61639 100644
--- a/vectordrawable/vectordrawable/build.gradle
+++ b/vectordrawable/vectordrawable/build.gradle
@@ -28,7 +28,7 @@
}
androidx {
- name = "Android Support VectorDrawable"
+ name = "VectorDrawable"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.VECTORDRAWABLE
inceptionYear = "2015"
diff --git a/viewpager/viewpager/api/api_lint.ignore b/viewpager/viewpager/api/api_lint.ignore
index 1c07b48..908ecb2 100644
--- a/viewpager/viewpager/api/api_lint.ignore
+++ b/viewpager/viewpager/api/api_lint.ignore
@@ -9,18 +9,12 @@
Symmetric method for `setDrawFullUnderline` must be named `isDrawFullUnderline`; was `getDrawFullUnderline`
-InvalidNullabilityOverride: androidx.viewpager.widget.PagerTabStrip#onDraw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `canvas` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.viewpager.widget.ViewPager#draw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `canvas` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.viewpager.widget.ViewPager#onDraw(android.graphics.Canvas) parameter #0:
- Invalid nullability on parameter `canvas` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-
-
ListenerInterface: androidx.viewpager.widget.ViewPager.SimpleOnPageChangeListener:
Listeners should be an interface, or otherwise renamed Callback: SimpleOnPageChangeListener
+MissingNullability: androidx.viewpager.widget.PagerTabStrip#onDraw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `onDraw`
MissingNullability: androidx.viewpager.widget.PagerTabStrip#onTouchEvent(android.view.MotionEvent) parameter #0:
Missing nullability on parameter `ev` in method `onTouchEvent`
MissingNullability: androidx.viewpager.widget.PagerTabStrip#setBackgroundDrawable(android.graphics.drawable.Drawable) parameter #0:
@@ -39,6 +33,8 @@
Missing nullability on parameter `event` in method `dispatchKeyEvent`
MissingNullability: androidx.viewpager.widget.ViewPager#dispatchPopulateAccessibilityEvent(android.view.accessibility.AccessibilityEvent) parameter #0:
Missing nullability on parameter `event` in method `dispatchPopulateAccessibilityEvent`
+MissingNullability: androidx.viewpager.widget.ViewPager#draw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `draw`
MissingNullability: androidx.viewpager.widget.ViewPager#generateDefaultLayoutParams():
Missing nullability on method `generateDefaultLayoutParams` return
MissingNullability: androidx.viewpager.widget.ViewPager#generateLayoutParams(android.util.AttributeSet):
@@ -49,6 +45,8 @@
Missing nullability on method `generateLayoutParams` return
MissingNullability: androidx.viewpager.widget.ViewPager#generateLayoutParams(android.view.ViewGroup.LayoutParams) parameter #0:
Missing nullability on parameter `p` in method `generateLayoutParams`
+MissingNullability: androidx.viewpager.widget.ViewPager#onDraw(android.graphics.Canvas) parameter #0:
+ Missing nullability on parameter `canvas` in method `onDraw`
MissingNullability: androidx.viewpager.widget.ViewPager#onInterceptTouchEvent(android.view.MotionEvent) parameter #0:
Missing nullability on parameter `ev` in method `onInterceptTouchEvent`
MissingNullability: androidx.viewpager.widget.ViewPager#onRequestFocusInDescendants(int, android.graphics.Rect) parameter #1:
diff --git a/viewpager/viewpager/build.gradle b/viewpager/viewpager/build.gradle
index 583c085..0bf2230 100644
--- a/viewpager/viewpager/build.gradle
+++ b/viewpager/viewpager/build.gradle
@@ -21,7 +21,7 @@
}
androidx {
- name = "Android Support Library View Pager"
+ name = "ViewPager"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 14 or later."
diff --git a/viewpager2/viewpager2/build.gradle b/viewpager2/viewpager2/build.gradle
index 7397c66..fcba271 100644
--- a/viewpager2/viewpager2/build.gradle
+++ b/viewpager2/viewpager2/build.gradle
@@ -52,7 +52,7 @@
}
androidx {
- name = "AndroidX Widget ViewPager2"
+ name = "ViewPager2"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "AndroidX Widget ViewPager2"
diff --git a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/TestActivity.kt b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/TestActivity.kt
index 7d31f03..a4f665f 100644
--- a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/TestActivity.kt
+++ b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/TestActivity.kt
@@ -32,6 +32,7 @@
onCreateCallback(this)
// disable enter animation.
+ @Suppress("Deprecation")
overridePendingTransition(0, 0)
}
@@ -39,6 +40,7 @@
super.finish()
// disable exit animation
+ @Suppress("Deprecation")
overridePendingTransition(0, 0)
}
diff --git a/wear/protolayout/protolayout-expression-pipeline/build.gradle b/wear/protolayout/protolayout-expression-pipeline/build.gradle
index d9db9ac..4f92627 100644
--- a/wear/protolayout/protolayout-expression-pipeline/build.gradle
+++ b/wear/protolayout/protolayout-expression-pipeline/build.gradle
@@ -50,7 +50,7 @@
}
androidx {
- name = "ProtoLayout Dynamic Expression Evaluation Pipeline"
+ name = "ProtoLayout Expression Pipeline"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2022"
description = "Evaluate dynamic expressions."
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/resources/robolectric.properties b/wear/protolayout/protolayout-expression-pipeline/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..34d9449
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until Wear team fixes their tests to work against sdk=33 (b/281072091).
+sdk=29
diff --git a/wear/protolayout/protolayout-expression/build.gradle b/wear/protolayout/protolayout-expression/build.gradle
index c9777d4..f0bf70b 100644
--- a/wear/protolayout/protolayout-expression/build.gradle
+++ b/wear/protolayout/protolayout-expression/build.gradle
@@ -49,7 +49,7 @@
}
androidx {
- name = "ProtoLayout Dynamic Expression"
+ name = "ProtoLayout Expression"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2022"
description = "Create dynamic expressions (for late evaluation by a remote evaluator)."
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java
index f87e5b5..188ecfa 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java
@@ -127,7 +127,7 @@
.toString())
.isEqualTo(
"Int32FormatOp{input=FixedInt32{value=1}, minIntegerDigits=2,"
- + " groupingUsed=true}");
+ + " groupingUsed=true}");
}
@Test
diff --git a/wear/protolayout/protolayout-expression/src/test/resources/robolectric.properties b/wear/protolayout/protolayout-expression/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..34d9449
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until Wear team fixes their tests to work against sdk=33 (b/281072091).
+sdk=29
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java
index 7e03861..54811fa 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java
@@ -73,6 +73,7 @@
return (int) ((px - 0.5f) / scale);
}
+ @SuppressWarnings("deprecation")
@Parameterized.Parameters(name = "{0}")
public static Collection<Object[]> data() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java
index 0089ecd..f47f90d 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java
@@ -73,6 +73,7 @@
return (int) ((px - 0.5f) / scale);
}
+ @SuppressWarnings("deprecation")
@Parameterized.Parameters(name = "{0}")
public static Collection<Object[]> data() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
diff --git a/wear/protolayout/protolayout-material/src/test/resources/robolectric.properties b/wear/protolayout/protolayout-material/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..34d9449
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until Wear team fixes their tests to work against sdk=33 (b/281072091).
+sdk=29
diff --git a/wear/protolayout/protolayout-renderer/src/test/resources/robolectric.properties b/wear/protolayout/protolayout-renderer/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..34d9449
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until Wear team fixes their tests to work against sdk=33 (b/281072091).
+sdk=29
diff --git a/wear/protolayout/protolayout/src/test/resources/robolectric.properties b/wear/protolayout/protolayout/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..34d9449
--- /dev/null
+++ b/wear/protolayout/protolayout/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until Wear team fixes their tests to work against sdk=33 (b/281072091).
+sdk=29
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java
index 0acea57..b30cd5a 100644
--- a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java
@@ -74,6 +74,8 @@
}
@Parameterized.Parameters(name = "{0}")
+ // TODO(b/267744228): Remove the warning suppression.
+ @SuppressWarnings("deprecation")
public static Collection<Object[]> data() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
DisplayMetrics currentDisplayMetrics = new DisplayMetrics();
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenXLTest.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenXLTest.java
index c4c4460..f449fcb 100644
--- a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenXLTest.java
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenXLTest.java
@@ -74,6 +74,8 @@
}
@Parameterized.Parameters(name = "{0}")
+ // TODO(b/267744228): Remove the warning suppression.
+ @SuppressWarnings("deprecation")
public static Collection<Object[]> data() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
DisplayMetrics currentDisplayMetrics = new DisplayMetrics();
diff --git a/wear/tiles/tiles-material/src/test/resources/robolectric.properties b/wear/tiles/tiles-material/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..34d9449
--- /dev/null
+++ b/wear/tiles/tiles-material/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until Wear team fixes their tests to work against sdk=33 (b/281072091).
+sdk=29
diff --git a/wear/tiles/tiles-renderer/src/test/resources/robolectric.properties b/wear/tiles/tiles-renderer/src/test/resources/robolectric.properties
index 71111c5..69fde47 100644
--- a/wear/tiles/tiles-renderer/src/test/resources/robolectric.properties
+++ b/wear/tiles/tiles-renderer/src/test/resources/robolectric.properties
@@ -1,17 +1,3 @@
-#
-# Copyright 2021 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/wear/tiles/tiles-testing/src/test/resources/robolectric.properties b/wear/tiles/tiles-testing/src/test/resources/robolectric.properties
index 27d4acf..69fde47 100644
--- a/wear/tiles/tiles-testing/src/test/resources/robolectric.properties
+++ b/wear/tiles/tiles-testing/src/test/resources/robolectric.properties
@@ -1,18 +1,3 @@
-#
-# Copyright 2021 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
# robolectric properties
-
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/wear/tiles/tiles/src/test/resources/robolectric.properties b/wear/tiles/tiles/src/test/resources/robolectric.properties
index 27d4acf..69fde47 100644
--- a/wear/tiles/tiles/src/test/resources/robolectric.properties
+++ b/wear/tiles/tiles/src/test/resources/robolectric.properties
@@ -1,18 +1,3 @@
-#
-# Copyright 2021 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
# robolectric properties
-
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/wear/watchface/watchface-client-guava/build.gradle b/wear/watchface/watchface-client-guava/build.gradle
index 49dba28..45693e3 100644
--- a/wear/watchface/watchface-client-guava/build.gradle
+++ b/wear/watchface/watchface-client-guava/build.gradle
@@ -40,7 +40,7 @@
}
androidx {
- name = "Android Wear Watchface Client Guava"
+ name = "Android Wear Watchface Client Guava Integration"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2021"
description = "Guava wrappers for the Androidx Wear Watchface library"
diff --git a/wear/watchface/watchface-client/src/test/resources/robolectric.properties b/wear/watchface/watchface-client/src/test/resources/robolectric.properties
index 80e2a6f..69fde47 100644
--- a/wear/watchface/watchface-client/src/test/resources/robolectric.properties
+++ b/wear/watchface/watchface-client/src/test/resources/robolectric.properties
@@ -1 +1,3 @@
# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/wear/watchface/watchface-complications-data-source-ktx/src/test/resources/robolectric.properties b/wear/watchface/watchface-complications-data-source-ktx/src/test/resources/robolectric.properties
index 4b7155e..69fde47 100644
--- a/wear/watchface/watchface-complications-data-source-ktx/src/test/resources/robolectric.properties
+++ b/wear/watchface/watchface-complications-data-source-ktx/src/test/resources/robolectric.properties
@@ -1,17 +1,3 @@
-#
-# Copyright 2021 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-# robolectric properties
\ No newline at end of file
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/wear/watchface/watchface-complications-data-source/src/test/resources/robolectric.properties b/wear/watchface/watchface-complications-data-source/src/test/resources/robolectric.properties
index 80e2a6f..69fde47 100644
--- a/wear/watchface/watchface-complications-data-source/src/test/resources/robolectric.properties
+++ b/wear/watchface/watchface-complications-data-source/src/test/resources/robolectric.properties
@@ -1 +1,3 @@
# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/wear/watchface/watchface-complications-data/src/test/resources/robolectric.properties b/wear/watchface/watchface-complications-data/src/test/resources/robolectric.properties
index ce87047..69fde47 100644
--- a/wear/watchface/watchface-complications-data/src/test/resources/robolectric.properties
+++ b/wear/watchface/watchface-complications-data/src/test/resources/robolectric.properties
@@ -1,3 +1,3 @@
-# Robolectric currently doesn't support API 30, so we have to explicitly specify 29 as the target
-# sdk for now. Remove when no longer necessary.
-sdk=29
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/wear/watchface/watchface-complications-permission-dialogs-sample/build.gradle b/wear/watchface/watchface-complications-permission-dialogs-sample/build.gradle
index 5342fd8..ecf03eb 100644
--- a/wear/watchface/watchface-complications-permission-dialogs-sample/build.gradle
+++ b/wear/watchface/watchface-complications-permission-dialogs-sample/build.gradle
@@ -29,7 +29,7 @@
}
androidx {
- name = "AndroidX Wear Watchface Permission Dialog Samples"
+ name = "Wear Watchface Permission Dialog Samples"
type = LibraryType.SAMPLES
inceptionYear = "2021"
description = "Contains sample code for building Watchface Permission Dialogs"
diff --git a/wear/watchface/watchface-complications-rendering/src/test/resources/robolectric.properties b/wear/watchface/watchface-complications-rendering/src/test/resources/robolectric.properties
index ce87047..69fde47 100644
--- a/wear/watchface/watchface-complications-rendering/src/test/resources/robolectric.properties
+++ b/wear/watchface/watchface-complications-rendering/src/test/resources/robolectric.properties
@@ -1,3 +1,3 @@
-# Robolectric currently doesn't support API 30, so we have to explicitly specify 29 as the target
-# sdk for now. Remove when no longer necessary.
-sdk=29
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/wear/watchface/watchface-complications/src/test/resources/robolectric.properties b/wear/watchface/watchface-complications/src/test/resources/robolectric.properties
index ce87047..34d9449 100644
--- a/wear/watchface/watchface-complications/src/test/resources/robolectric.properties
+++ b/wear/watchface/watchface-complications/src/test/resources/robolectric.properties
@@ -1,3 +1,3 @@
-# Robolectric currently doesn't support API 30, so we have to explicitly specify 29 as the target
-# sdk for now. Remove when no longer necessary.
+# robolectric properties
+# Temporary until Wear team fixes their tests to work against sdk=33 (b/281072091).
sdk=29
diff --git a/wear/watchface/watchface-editor-guava/build.gradle b/wear/watchface/watchface-editor-guava/build.gradle
index c8cb24c..933c2fc 100644
--- a/wear/watchface/watchface-editor-guava/build.gradle
+++ b/wear/watchface/watchface-editor-guava/build.gradle
@@ -38,7 +38,7 @@
}
androidx {
- name = "Android Wear Watchface Client Editor"
+ name = "Android Wear Watchface Editor Guava Integration"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2021"
description = "Guava wrappers for the Androidx Wear Watchface Editor library"
diff --git a/wear/watchface/watchface-editor/samples/build.gradle b/wear/watchface/watchface-editor/samples/build.gradle
index 85c5449..4fa0e56 100644
--- a/wear/watchface/watchface-editor/samples/build.gradle
+++ b/wear/watchface/watchface-editor/samples/build.gradle
@@ -32,7 +32,7 @@
}
androidx {
- name = "AndroidX Wear Editor Samples"
+ name = "Wear Editor Samples"
type = LibraryType.SAMPLES
inceptionYear = "2020"
description = "Contains sample code for the Androidx Wear Editor library"
diff --git a/wear/watchface/watchface-guava/build.gradle b/wear/watchface/watchface-guava/build.gradle
index 6571f63..c72486d 100644
--- a/wear/watchface/watchface-guava/build.gradle
+++ b/wear/watchface/watchface-guava/build.gradle
@@ -45,7 +45,7 @@
}
androidx {
- name = "AndroidX Wear Watchface Guava"
+ name = "Android Wear Watchface Guava Integration"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2021"
description = "Guava wrappers for the Androidx Wear Watchface library"
diff --git a/wear/watchface/watchface-guava/src/test/resources/robolectric.properties b/wear/watchface/watchface-guava/src/test/resources/robolectric.properties
index 80e2a6f..69fde47 100644
--- a/wear/watchface/watchface-guava/src/test/resources/robolectric.properties
+++ b/wear/watchface/watchface-guava/src/test/resources/robolectric.properties
@@ -1 +1,3 @@
# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/wear/watchface/watchface-samples-minimal-complications/build.gradle b/wear/watchface/watchface-samples-minimal-complications/build.gradle
index ce74910..eb6c3db 100644
--- a/wear/watchface/watchface-samples-minimal-complications/build.gradle
+++ b/wear/watchface/watchface-samples-minimal-complications/build.gradle
@@ -35,7 +35,7 @@
}
androidx {
- name = "AndroidX Wear Watchface Minimal Complications Sample"
+ name = "Wear Watchface Minimal Complications Sample"
type = LibraryType.SAMPLES
inceptionYear = "2021"
description = "Contains the sample code for the Androidx Wear Watchface library"
diff --git a/wear/watchface/watchface-samples-minimal-instances/build.gradle b/wear/watchface/watchface-samples-minimal-instances/build.gradle
index 9fd911c..cefa848 100644
--- a/wear/watchface/watchface-samples-minimal-instances/build.gradle
+++ b/wear/watchface/watchface-samples-minimal-instances/build.gradle
@@ -34,7 +34,7 @@
}
androidx {
- name = "AndroidX Wear Watchface Minimal Style Sample"
+ name = "Wear Watchface Minimal Style Sample"
type = LibraryType.SAMPLES
inceptionYear = "2021"
description = "Contains the sample code for the Androidx Wear Watchface library"
diff --git a/wear/watchface/watchface-samples-minimal-style/build.gradle b/wear/watchface/watchface-samples-minimal-style/build.gradle
index 3931645..76d81e8 100644
--- a/wear/watchface/watchface-samples-minimal-style/build.gradle
+++ b/wear/watchface/watchface-samples-minimal-style/build.gradle
@@ -32,7 +32,7 @@
}
androidx {
- name = "AndroidX Wear Watchface Minimal Style Sample"
+ name = "Wear Watchface Minimal Style Sample"
type = LibraryType.SAMPLES
inceptionYear = "2021"
description = "Contains the sample code for the Androidx Wear Watchface library"
diff --git a/wear/watchface/watchface-style/old-api-test-service/build.gradle b/wear/watchface/watchface-style/old-api-test-service/build.gradle
index 6d668ff..905cc31 100644
--- a/wear/watchface/watchface-style/old-api-test-service/build.gradle
+++ b/wear/watchface/watchface-style/old-api-test-service/build.gradle
@@ -35,7 +35,7 @@
}
androidx {
- name = "AndroidX WatchFace Style Old Api Test Service"
+ name = "WatchFace Style Old Api Test Service"
type = LibraryType.SAMPLES
inceptionYear = "2022"
description = "Test service built with v1.0.0 of the API, used to check for binary AIDL compat"
diff --git a/wear/watchface/watchface-style/old-api-test-stub/build.gradle b/wear/watchface/watchface-style/old-api-test-stub/build.gradle
index ba36ffc..aa784be 100644
--- a/wear/watchface/watchface-style/old-api-test-stub/build.gradle
+++ b/wear/watchface/watchface-style/old-api-test-stub/build.gradle
@@ -32,7 +32,7 @@
}
androidx {
- name = "AndroidX WatchFace Style Old Api Test Stub"
+ name = "WatchFace Style Old Api Test Stub"
type = LibraryType.INTERNAL_TEST_LIBRARY
inceptionYear = "2022"
description = "Test stub built with v1.0.0 of the API, used to check for binary AIDL compat"
diff --git a/wear/watchface/watchface-style/src/test/resources/robolectric.properties b/wear/watchface/watchface-style/src/test/resources/robolectric.properties
index 80e2a6f..69fde47 100644
--- a/wear/watchface/watchface-style/src/test/resources/robolectric.properties
+++ b/wear/watchface/watchface-style/src/test/resources/robolectric.properties
@@ -1 +1,3 @@
# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/wear/watchface/watchface/samples/app/build.gradle b/wear/watchface/watchface/samples/app/build.gradle
index 875f848..101b6ba 100644
--- a/wear/watchface/watchface/samples/app/build.gradle
+++ b/wear/watchface/watchface/samples/app/build.gradle
@@ -26,7 +26,7 @@
}
androidx {
- name = "AndroidX Wear Watchface Samples app"
+ name = "Wear Watchface Samples app"
type = LibraryType.SAMPLES
inceptionYear = "2021"
description = "APK for the sample code for the Androidx Wear Watchface library"
diff --git a/wear/watchface/watchface/samples/build.gradle b/wear/watchface/watchface/samples/build.gradle
index 0e89628..f7b78e9 100644
--- a/wear/watchface/watchface/samples/build.gradle
+++ b/wear/watchface/watchface/samples/build.gradle
@@ -33,7 +33,7 @@
}
androidx {
- name = "AndroidX Wear Watchface Samples"
+ name = "Wear Watchface Samples"
type = LibraryType.SAMPLES
inceptionYear = "2020"
description = "Contains the sample code for the Androidx Wear Watchface library"
diff --git a/wear/watchface/watchface/samples/minimal/build.gradle b/wear/watchface/watchface/samples/minimal/build.gradle
index 38d5ed5..c8e5139 100644
--- a/wear/watchface/watchface/samples/minimal/build.gradle
+++ b/wear/watchface/watchface/samples/minimal/build.gradle
@@ -28,7 +28,7 @@
}
androidx {
- name = "AndroidX Wear Watchface Minimal Sample"
+ name = "Wear Watchface Minimal Sample"
type = LibraryType.SAMPLES
inceptionYear = "2021"
description = "Contains the sample code for the Androidx Wear Watchface library"
diff --git a/wear/watchface/watchface/src/test/resources/robolectric.properties b/wear/watchface/watchface/src/test/resources/robolectric.properties
index ce87047..69fde47 100644
--- a/wear/watchface/watchface/src/test/resources/robolectric.properties
+++ b/wear/watchface/watchface/src/test/resources/robolectric.properties
@@ -1,3 +1,3 @@
-# Robolectric currently doesn't support API 30, so we have to explicitly specify 29 as the target
-# sdk for now. Remove when no longer necessary.
-sdk=29
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/wear/wear-input-testing/build.gradle b/wear/wear-input-testing/build.gradle
index 6952126..d422e13 100644
--- a/wear/wear-input-testing/build.gradle
+++ b/wear/wear-input-testing/build.gradle
@@ -34,9 +34,9 @@
}
androidx {
- name = "Android Wear Support Input Testing Helpers"
+ name = "Android Wear Input Testing Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.WEAR_INPUT_TESTING
inceptionYear = "2020"
- description = "Android Wear Support Input Testing Helpers"
+ description = "Android Wear Support Input Testing Helpers"
}
diff --git a/wear/wear-input/build.gradle b/wear/wear-input/build.gradle
index 8301f64d..5b5c6d9 100644
--- a/wear/wear-input/build.gradle
+++ b/wear/wear-input/build.gradle
@@ -51,7 +51,7 @@
}
androidx {
- name = "Android Wear Support Input"
+ name = "Android Wear Input"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.WEAR_INPUT
inceptionYear = "2020"
diff --git a/wear/wear-input/src/test/resources/robolectric.properties b/wear/wear-input/src/test/resources/robolectric.properties
index 4b7155e..69fde47 100644
--- a/wear/wear-input/src/test/resources/robolectric.properties
+++ b/wear/wear-input/src/test/resources/robolectric.properties
@@ -1,17 +1,3 @@
-#
-# Copyright 2021 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-# robolectric properties
\ No newline at end of file
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/wear/wear-ongoing/build.gradle b/wear/wear-ongoing/build.gradle
index 8baebed..0985190 100644
--- a/wear/wear-ongoing/build.gradle
+++ b/wear/wear-ongoing/build.gradle
@@ -32,7 +32,7 @@
}
androidx {
- name = "Android Wear Ongoing Activities"
+ name = "Android Wear Ongoing"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.WEAR_ONGOING
inceptionYear = "2021"
diff --git a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthClient.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthClient.kt
index 23cfd65..c564a62 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthClient.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthClient.kt
@@ -161,11 +161,11 @@
return RemoteAuthClient(
object : ServiceBinder {
override fun bindService(
- intent: Intent?,
- connection: ServiceConnection?,
+ intent: Intent,
+ connection: ServiceConnection,
flags: Int
): Boolean {
- return appContext.bindService(intent, connection!!, flags)
+ return appContext.bindService(intent, connection, flags)
}
override fun unbindService(connection: ServiceConnection?) {
@@ -276,7 +276,7 @@
internal interface ServiceBinder {
/** See [Context.bindService]. */
- fun bindService(intent: Intent?, connection: ServiceConnection?, flags: Int): Boolean
+ fun bindService(intent: Intent, connection: ServiceConnection, flags: Int): Boolean
/** See [Context.unbindService]. */
fun unbindService(connection: ServiceConnection?)
diff --git a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/RemoteAuthTest.kt b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/RemoteAuthTest.kt
index 2d0b389..acdba35 100644
--- a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/RemoteAuthTest.kt
+++ b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/RemoteAuthTest.kt
@@ -209,11 +209,11 @@
var state = ConnectionState.DISCONNECTED
private var serviceConnection: ServiceConnection? = null
override fun bindService(
- intent: Intent?,
- connection: ServiceConnection?,
+ intent: Intent,
+ connection: ServiceConnection,
flags: Int
): Boolean {
- if (intent!!.getPackage() != RemoteAuthClient.WEARABLE_PACKAGE_NAME) {
+ if (intent.getPackage() != RemoteAuthClient.WEARABLE_PACKAGE_NAME) {
throw UnsupportedOperationException()
}
if (intent.action != RemoteAuthClient.ACTION_AUTH) {
diff --git a/wear/wear/build.gradle b/wear/wear/build.gradle
index 83bcb50..ea42582 100644
--- a/wear/wear/build.gradle
+++ b/wear/wear/build.gradle
@@ -62,7 +62,7 @@
}
androidx {
- name = "Android Wear Support UI"
+ name = "Android Wear"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenVersion = LibraryVersions.WEAR
inceptionYear = "2016"
diff --git a/wear/wear/src/test/resources/robolectric.properties b/wear/wear/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..69fde47
--- /dev/null
+++ b/wear/wear/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/webkit/webkit/api/1.6.0-beta02.txt b/webkit/webkit/api/1.6.0-beta02.txt
deleted file mode 100644
index faf13cb..0000000
--- a/webkit/webkit/api/1.6.0-beta02.txt
+++ /dev/null
@@ -1,300 +0,0 @@
-// Signature format: 4.0
-package androidx.webkit {
-
- public class CookieManagerCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_COOKIE_INFO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.List<java.lang.String!> getCookieInfo(android.webkit.CookieManager, String);
- }
-
- public abstract class JavaScriptReplyProxy {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
- }
-
- public class ProcessGlobalConfig {
- ctor public ProcessGlobalConfig();
- method public static void apply(androidx.webkit.ProcessGlobalConfig);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
- }
-
- public final class ProxyConfig {
- method public java.util.List<java.lang.String!> getBypassRules();
- method public java.util.List<androidx.webkit.ProxyConfig.ProxyRule!> getProxyRules();
- method public boolean isReverseBypassEnabled();
- field public static final String MATCH_ALL_SCHEMES = "*";
- field public static final String MATCH_HTTP = "http";
- field public static final String MATCH_HTTPS = "https";
- }
-
- public static final class ProxyConfig.Builder {
- ctor public ProxyConfig.Builder();
- ctor public ProxyConfig.Builder(androidx.webkit.ProxyConfig);
- method public androidx.webkit.ProxyConfig.Builder addBypassRule(String);
- method public androidx.webkit.ProxyConfig.Builder addDirect(String);
- method public androidx.webkit.ProxyConfig.Builder addDirect();
- method public androidx.webkit.ProxyConfig.Builder addProxyRule(String);
- method public androidx.webkit.ProxyConfig.Builder addProxyRule(String, String);
- method public androidx.webkit.ProxyConfig build();
- method public androidx.webkit.ProxyConfig.Builder bypassSimpleHostnames();
- method public androidx.webkit.ProxyConfig.Builder removeImplicitRules();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.PROXY_OVERRIDE_REVERSE_BYPASS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public androidx.webkit.ProxyConfig.Builder setReverseBypassEnabled(boolean);
- }
-
- public static final class ProxyConfig.ProxyRule {
- method public String getSchemeFilter();
- method public String getUrl();
- }
-
- public abstract class ProxyController {
- method public abstract void clearProxyOverride(java.util.concurrent.Executor, Runnable);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.PROXY_OVERRIDE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.ProxyController getInstance();
- method public abstract void setProxyOverride(androidx.webkit.ProxyConfig, java.util.concurrent.Executor, Runnable);
- }
-
- public abstract class SafeBrowsingResponseCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void backToSafety(boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void proceed(boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void showInterstitial(boolean);
- }
-
- public abstract class ServiceWorkerClientCompat {
- ctor public ServiceWorkerClientCompat();
- method @WorkerThread public abstract android.webkit.WebResourceResponse? shouldInterceptRequest(android.webkit.WebResourceRequest);
- }
-
- public abstract class ServiceWorkerControllerCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.ServiceWorkerControllerCompat getInstance();
- method public abstract androidx.webkit.ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings();
- method public abstract void setServiceWorkerClient(androidx.webkit.ServiceWorkerClientCompat?);
- }
-
- public abstract class ServiceWorkerWebSettingsCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowContentAccess();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowFileAccess();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getBlockNetworkLoads();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getCacheMode();
- method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowContentAccess(boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowFileAccess(boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setBlockNetworkLoads(boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setCacheMode(int);
- method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String!>);
- }
-
- public class TracingConfig {
- method public java.util.List<java.lang.String!> getCustomIncludedCategories();
- method public int getPredefinedCategories();
- method public int getTracingMode();
- field public static final int CATEGORIES_ALL = 1; // 0x1
- field public static final int CATEGORIES_ANDROID_WEBVIEW = 2; // 0x2
- field public static final int CATEGORIES_FRAME_VIEWER = 64; // 0x40
- field public static final int CATEGORIES_INPUT_LATENCY = 8; // 0x8
- field public static final int CATEGORIES_JAVASCRIPT_AND_RENDERING = 32; // 0x20
- field public static final int CATEGORIES_NONE = 0; // 0x0
- field public static final int CATEGORIES_RENDERING = 16; // 0x10
- field public static final int CATEGORIES_WEB_DEVELOPER = 4; // 0x4
- field public static final int RECORD_CONTINUOUSLY = 1; // 0x1
- field public static final int RECORD_UNTIL_FULL = 0; // 0x0
- }
-
- public static class TracingConfig.Builder {
- ctor public TracingConfig.Builder();
- method public androidx.webkit.TracingConfig.Builder addCategories(int...);
- method public androidx.webkit.TracingConfig.Builder addCategories(java.lang.String!...);
- method public androidx.webkit.TracingConfig.Builder addCategories(java.util.Collection<java.lang.String!>);
- method public androidx.webkit.TracingConfig build();
- method public androidx.webkit.TracingConfig.Builder setTracingMode(int);
- }
-
- public abstract class TracingController {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.TRACING_CONTROLLER_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.TracingController getInstance();
- method public abstract boolean isTracing();
- method public abstract void start(androidx.webkit.TracingConfig);
- method public abstract boolean stop(java.io.OutputStream?, java.util.concurrent.Executor);
- }
-
- public class WebMessageCompat {
- ctor public WebMessageCompat(String?);
- ctor public WebMessageCompat(String?, androidx.webkit.WebMessagePortCompat![]?);
- method public String? getData();
- method public androidx.webkit.WebMessagePortCompat![]? getPorts();
- }
-
- public abstract class WebMessagePortCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_CLOSE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void close();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_POST_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(androidx.webkit.WebMessageCompat);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setWebMessageCallback(androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setWebMessageCallback(android.os.Handler?, androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat);
- }
-
- public abstract static class WebMessagePortCompat.WebMessageCallbackCompat {
- ctor public WebMessagePortCompat.WebMessageCallbackCompat();
- method public void onMessage(androidx.webkit.WebMessagePortCompat, androidx.webkit.WebMessageCompat?);
- }
-
- public abstract class WebResourceErrorCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract CharSequence getDescription();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getErrorCode();
- }
-
- public class WebResourceRequestCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isRedirect(android.webkit.WebResourceRequest);
- }
-
- public class WebSettingsCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getDisabledActionModeMenuItems(android.webkit.WebSettings);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getEnterpriseAuthenticationAppLinkPolicyEnabled(android.webkit.WebSettings);
- method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDark(android.webkit.WebSettings);
- method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDarkStrategy(android.webkit.WebSettings);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getOffscreenPreRaster(android.webkit.WebSettings);
- method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList(android.webkit.WebSettings);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getSafeBrowsingEnabled(android.webkit.WebSettings);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isAlgorithmicDarkeningAllowed(android.webkit.WebSettings);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setAlgorithmicDarkeningAllowed(android.webkit.WebSettings, boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setDisabledActionModeMenuItems(android.webkit.WebSettings, int);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setEnterpriseAuthenticationAppLinkPolicyEnabled(android.webkit.WebSettings, boolean);
- method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDark(android.webkit.WebSettings, int);
- method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDarkStrategy(android.webkit.WebSettings, int);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setOffscreenPreRaster(android.webkit.WebSettings, boolean);
- method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setRequestedWithHeaderOriginAllowList(android.webkit.WebSettings, java.util.Set<java.lang.String!>);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingEnabled(android.webkit.WebSettings, boolean);
- field @Deprecated public static final int DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING = 2; // 0x2
- field @Deprecated public static final int DARK_STRATEGY_USER_AGENT_DARKENING_ONLY = 0; // 0x0
- field @Deprecated public static final int DARK_STRATEGY_WEB_THEME_DARKENING_ONLY = 1; // 0x1
- field @Deprecated public static final int FORCE_DARK_AUTO = 1; // 0x1
- field @Deprecated public static final int FORCE_DARK_OFF = 0; // 0x0
- field @Deprecated public static final int FORCE_DARK_ON = 2; // 0x2
- }
-
- public final class WebViewAssetLoader {
- method @WorkerThread public android.webkit.WebResourceResponse? shouldInterceptRequest(android.net.Uri);
- field public static final String DEFAULT_DOMAIN = "appassets.androidplatform.net";
- }
-
- public static final class WebViewAssetLoader.AssetsPathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
- ctor public WebViewAssetLoader.AssetsPathHandler(android.content.Context);
- method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
- }
-
- public static final class WebViewAssetLoader.Builder {
- ctor public WebViewAssetLoader.Builder();
- method public androidx.webkit.WebViewAssetLoader.Builder addPathHandler(String, androidx.webkit.WebViewAssetLoader.PathHandler);
- method public androidx.webkit.WebViewAssetLoader build();
- method public androidx.webkit.WebViewAssetLoader.Builder setDomain(String);
- method public androidx.webkit.WebViewAssetLoader.Builder setHttpAllowed(boolean);
- }
-
- public static final class WebViewAssetLoader.InternalStoragePathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
- ctor public WebViewAssetLoader.InternalStoragePathHandler(android.content.Context, java.io.File);
- method @WorkerThread public android.webkit.WebResourceResponse handle(String);
- }
-
- public static interface WebViewAssetLoader.PathHandler {
- method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
- }
-
- public static final class WebViewAssetLoader.ResourcesPathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
- ctor public WebViewAssetLoader.ResourcesPathHandler(android.content.Context);
- method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
- }
-
- public class WebViewClientCompat extends android.webkit.WebViewClient {
- ctor public WebViewClientCompat();
- method @RequiresApi(23) public final void onReceivedError(android.webkit.WebView, android.webkit.WebResourceRequest, android.webkit.WebResourceError);
- method @RequiresApi(21) @UiThread public void onReceivedError(android.webkit.WebView, android.webkit.WebResourceRequest, androidx.webkit.WebResourceErrorCompat);
- method @RequiresApi(27) public final void onSafeBrowsingHit(android.webkit.WebView, android.webkit.WebResourceRequest, int, android.webkit.SafeBrowsingResponse);
- method @UiThread public void onSafeBrowsingHit(android.webkit.WebView, android.webkit.WebResourceRequest, int, androidx.webkit.SafeBrowsingResponseCompat);
- }
-
- public class WebViewCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void addWebMessageListener(android.webkit.WebView, String, java.util.Set<java.lang.String!>, androidx.webkit.WebViewCompat.WebMessageListener);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebMessagePortCompat![] createWebMessageChannel(android.webkit.WebView);
- method public static android.content.pm.PackageInfo? getCurrentWebViewPackage(android.content.Context);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.net.Uri getSafeBrowsingPrivacyPolicyUrl();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_VARIATIONS_HEADER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static String getVariationsHeader();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_CHROME_CLIENT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.webkit.WebChromeClient? getWebChromeClient(android.webkit.WebView);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_VIEW_CLIENT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.webkit.WebViewClient getWebViewClient(android.webkit.WebView);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_VIEW_RENDERER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebViewRenderProcess? getWebViewRenderProcess(android.webkit.WebView);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebViewRenderProcessClient? getWebViewRenderProcessClient(android.webkit.WebView);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isMultiProcessEnabled();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.VISUAL_STATE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void postVisualStateCallback(android.webkit.WebView, long, androidx.webkit.WebViewCompat.VisualStateCallback);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.POST_WEB_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void postWebMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void removeWebMessageListener(android.webkit.WebView, String);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ALLOWLIST, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingAllowlist(java.util.Set<java.lang.String!>, android.webkit.ValueCallback<java.lang.Boolean!>?);
- method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_WHITELIST, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingWhitelist(java.util.List<java.lang.String!>, android.webkit.ValueCallback<java.lang.Boolean!>?);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setWebViewRenderProcessClient(android.webkit.WebView, java.util.concurrent.Executor, androidx.webkit.WebViewRenderProcessClient);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setWebViewRenderProcessClient(android.webkit.WebView, androidx.webkit.WebViewRenderProcessClient?);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.START_SAFE_BROWSING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void startSafeBrowsing(android.content.Context, android.webkit.ValueCallback<java.lang.Boolean!>?);
- }
-
- public static interface WebViewCompat.VisualStateCallback {
- method @UiThread public void onComplete(long);
- }
-
- public static interface WebViewCompat.WebMessageListener {
- method @UiThread public void onPostMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri, boolean, androidx.webkit.JavaScriptReplyProxy);
- }
-
- public class WebViewFeature {
- method public static boolean isFeatureSupported(String);
- method public static boolean isStartupFeatureSupported(android.content.Context, String);
- field public static final String ALGORITHMIC_DARKENING = "ALGORITHMIC_DARKENING";
- field public static final String CREATE_WEB_MESSAGE_CHANNEL = "CREATE_WEB_MESSAGE_CHANNEL";
- field public static final String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
- field public static final String ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY = "ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY";
- field public static final String FORCE_DARK = "FORCE_DARK";
- field public static final String FORCE_DARK_STRATEGY = "FORCE_DARK_STRATEGY";
- field public static final String GET_COOKIE_INFO = "GET_COOKIE_INFO";
- field public static final String GET_VARIATIONS_HEADER = "GET_VARIATIONS_HEADER";
- field public static final String GET_WEB_CHROME_CLIENT = "GET_WEB_CHROME_CLIENT";
- field public static final String GET_WEB_VIEW_CLIENT = "GET_WEB_VIEW_CLIENT";
- field public static final String GET_WEB_VIEW_RENDERER = "GET_WEB_VIEW_RENDERER";
- field public static final String MULTI_PROCESS = "MULTI_PROCESS";
- field public static final String OFF_SCREEN_PRERASTER = "OFF_SCREEN_PRERASTER";
- field public static final String POST_WEB_MESSAGE = "POST_WEB_MESSAGE";
- field public static final String PROXY_OVERRIDE = "PROXY_OVERRIDE";
- field public static final String PROXY_OVERRIDE_REVERSE_BYPASS = "PROXY_OVERRIDE_REVERSE_BYPASS";
- field public static final String RECEIVE_HTTP_ERROR = "RECEIVE_HTTP_ERROR";
- field public static final String RECEIVE_WEB_RESOURCE_ERROR = "RECEIVE_WEB_RESOURCE_ERROR";
- field public static final String SAFE_BROWSING_ALLOWLIST = "SAFE_BROWSING_ALLOWLIST";
- field public static final String SAFE_BROWSING_ENABLE = "SAFE_BROWSING_ENABLE";
- field public static final String SAFE_BROWSING_HIT = "SAFE_BROWSING_HIT";
- field public static final String SAFE_BROWSING_PRIVACY_POLICY_URL = "SAFE_BROWSING_PRIVACY_POLICY_URL";
- field public static final String SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY = "SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY";
- field public static final String SAFE_BROWSING_RESPONSE_PROCEED = "SAFE_BROWSING_RESPONSE_PROCEED";
- field public static final String SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL = "SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL";
- field @Deprecated public static final String SAFE_BROWSING_WHITELIST = "SAFE_BROWSING_WHITELIST";
- field public static final String SERVICE_WORKER_BASIC_USAGE = "SERVICE_WORKER_BASIC_USAGE";
- field public static final String SERVICE_WORKER_BLOCK_NETWORK_LOADS = "SERVICE_WORKER_BLOCK_NETWORK_LOADS";
- field public static final String SERVICE_WORKER_CACHE_MODE = "SERVICE_WORKER_CACHE_MODE";
- field public static final String SERVICE_WORKER_CONTENT_ACCESS = "SERVICE_WORKER_CONTENT_ACCESS";
- field public static final String SERVICE_WORKER_FILE_ACCESS = "SERVICE_WORKER_FILE_ACCESS";
- field public static final String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = "SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST";
- field public static final String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
- field public static final String STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX = "STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX";
- field public static final String START_SAFE_BROWSING = "START_SAFE_BROWSING";
- field public static final String TRACING_CONTROLLER_BASIC_USAGE = "TRACING_CONTROLLER_BASIC_USAGE";
- field public static final String VISUAL_STATE_CALLBACK = "VISUAL_STATE_CALLBACK";
- field public static final String WEB_MESSAGE_CALLBACK_ON_MESSAGE = "WEB_MESSAGE_CALLBACK_ON_MESSAGE";
- field public static final String WEB_MESSAGE_LISTENER = "WEB_MESSAGE_LISTENER";
- field public static final String WEB_MESSAGE_PORT_CLOSE = "WEB_MESSAGE_PORT_CLOSE";
- field public static final String WEB_MESSAGE_PORT_POST_MESSAGE = "WEB_MESSAGE_PORT_POST_MESSAGE";
- field public static final String WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK = "WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK";
- field public static final String WEB_RESOURCE_ERROR_GET_CODE = "WEB_RESOURCE_ERROR_GET_CODE";
- field public static final String WEB_RESOURCE_ERROR_GET_DESCRIPTION = "WEB_RESOURCE_ERROR_GET_DESCRIPTION";
- field public static final String WEB_RESOURCE_REQUEST_IS_REDIRECT = "WEB_RESOURCE_REQUEST_IS_REDIRECT";
- field public static final String WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE = "WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE";
- field public static final String WEB_VIEW_RENDERER_TERMINATE = "WEB_VIEW_RENDERER_TERMINATE";
- }
-
- public abstract class WebViewRenderProcess {
- ctor public WebViewRenderProcess();
- method public abstract boolean terminate();
- }
-
- public abstract class WebViewRenderProcessClient {
- ctor public WebViewRenderProcessClient();
- method public abstract void onRenderProcessResponsive(android.webkit.WebView, androidx.webkit.WebViewRenderProcess?);
- method public abstract void onRenderProcessUnresponsive(android.webkit.WebView, androidx.webkit.WebViewRenderProcess?);
- }
-
-}
-
diff --git a/webkit/webkit/api/public_plus_experimental_1.6.0-beta02.txt b/webkit/webkit/api/public_plus_experimental_1.6.0-beta02.txt
deleted file mode 100644
index faf13cb..0000000
--- a/webkit/webkit/api/public_plus_experimental_1.6.0-beta02.txt
+++ /dev/null
@@ -1,300 +0,0 @@
-// Signature format: 4.0
-package androidx.webkit {
-
- public class CookieManagerCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_COOKIE_INFO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.List<java.lang.String!> getCookieInfo(android.webkit.CookieManager, String);
- }
-
- public abstract class JavaScriptReplyProxy {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
- }
-
- public class ProcessGlobalConfig {
- ctor public ProcessGlobalConfig();
- method public static void apply(androidx.webkit.ProcessGlobalConfig);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
- }
-
- public final class ProxyConfig {
- method public java.util.List<java.lang.String!> getBypassRules();
- method public java.util.List<androidx.webkit.ProxyConfig.ProxyRule!> getProxyRules();
- method public boolean isReverseBypassEnabled();
- field public static final String MATCH_ALL_SCHEMES = "*";
- field public static final String MATCH_HTTP = "http";
- field public static final String MATCH_HTTPS = "https";
- }
-
- public static final class ProxyConfig.Builder {
- ctor public ProxyConfig.Builder();
- ctor public ProxyConfig.Builder(androidx.webkit.ProxyConfig);
- method public androidx.webkit.ProxyConfig.Builder addBypassRule(String);
- method public androidx.webkit.ProxyConfig.Builder addDirect(String);
- method public androidx.webkit.ProxyConfig.Builder addDirect();
- method public androidx.webkit.ProxyConfig.Builder addProxyRule(String);
- method public androidx.webkit.ProxyConfig.Builder addProxyRule(String, String);
- method public androidx.webkit.ProxyConfig build();
- method public androidx.webkit.ProxyConfig.Builder bypassSimpleHostnames();
- method public androidx.webkit.ProxyConfig.Builder removeImplicitRules();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.PROXY_OVERRIDE_REVERSE_BYPASS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public androidx.webkit.ProxyConfig.Builder setReverseBypassEnabled(boolean);
- }
-
- public static final class ProxyConfig.ProxyRule {
- method public String getSchemeFilter();
- method public String getUrl();
- }
-
- public abstract class ProxyController {
- method public abstract void clearProxyOverride(java.util.concurrent.Executor, Runnable);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.PROXY_OVERRIDE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.ProxyController getInstance();
- method public abstract void setProxyOverride(androidx.webkit.ProxyConfig, java.util.concurrent.Executor, Runnable);
- }
-
- public abstract class SafeBrowsingResponseCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void backToSafety(boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void proceed(boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void showInterstitial(boolean);
- }
-
- public abstract class ServiceWorkerClientCompat {
- ctor public ServiceWorkerClientCompat();
- method @WorkerThread public abstract android.webkit.WebResourceResponse? shouldInterceptRequest(android.webkit.WebResourceRequest);
- }
-
- public abstract class ServiceWorkerControllerCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.ServiceWorkerControllerCompat getInstance();
- method public abstract androidx.webkit.ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings();
- method public abstract void setServiceWorkerClient(androidx.webkit.ServiceWorkerClientCompat?);
- }
-
- public abstract class ServiceWorkerWebSettingsCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowContentAccess();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowFileAccess();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getBlockNetworkLoads();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getCacheMode();
- method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowContentAccess(boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowFileAccess(boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setBlockNetworkLoads(boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setCacheMode(int);
- method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String!>);
- }
-
- public class TracingConfig {
- method public java.util.List<java.lang.String!> getCustomIncludedCategories();
- method public int getPredefinedCategories();
- method public int getTracingMode();
- field public static final int CATEGORIES_ALL = 1; // 0x1
- field public static final int CATEGORIES_ANDROID_WEBVIEW = 2; // 0x2
- field public static final int CATEGORIES_FRAME_VIEWER = 64; // 0x40
- field public static final int CATEGORIES_INPUT_LATENCY = 8; // 0x8
- field public static final int CATEGORIES_JAVASCRIPT_AND_RENDERING = 32; // 0x20
- field public static final int CATEGORIES_NONE = 0; // 0x0
- field public static final int CATEGORIES_RENDERING = 16; // 0x10
- field public static final int CATEGORIES_WEB_DEVELOPER = 4; // 0x4
- field public static final int RECORD_CONTINUOUSLY = 1; // 0x1
- field public static final int RECORD_UNTIL_FULL = 0; // 0x0
- }
-
- public static class TracingConfig.Builder {
- ctor public TracingConfig.Builder();
- method public androidx.webkit.TracingConfig.Builder addCategories(int...);
- method public androidx.webkit.TracingConfig.Builder addCategories(java.lang.String!...);
- method public androidx.webkit.TracingConfig.Builder addCategories(java.util.Collection<java.lang.String!>);
- method public androidx.webkit.TracingConfig build();
- method public androidx.webkit.TracingConfig.Builder setTracingMode(int);
- }
-
- public abstract class TracingController {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.TRACING_CONTROLLER_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.TracingController getInstance();
- method public abstract boolean isTracing();
- method public abstract void start(androidx.webkit.TracingConfig);
- method public abstract boolean stop(java.io.OutputStream?, java.util.concurrent.Executor);
- }
-
- public class WebMessageCompat {
- ctor public WebMessageCompat(String?);
- ctor public WebMessageCompat(String?, androidx.webkit.WebMessagePortCompat![]?);
- method public String? getData();
- method public androidx.webkit.WebMessagePortCompat![]? getPorts();
- }
-
- public abstract class WebMessagePortCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_CLOSE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void close();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_POST_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(androidx.webkit.WebMessageCompat);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setWebMessageCallback(androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setWebMessageCallback(android.os.Handler?, androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat);
- }
-
- public abstract static class WebMessagePortCompat.WebMessageCallbackCompat {
- ctor public WebMessagePortCompat.WebMessageCallbackCompat();
- method public void onMessage(androidx.webkit.WebMessagePortCompat, androidx.webkit.WebMessageCompat?);
- }
-
- public abstract class WebResourceErrorCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract CharSequence getDescription();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getErrorCode();
- }
-
- public class WebResourceRequestCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isRedirect(android.webkit.WebResourceRequest);
- }
-
- public class WebSettingsCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getDisabledActionModeMenuItems(android.webkit.WebSettings);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getEnterpriseAuthenticationAppLinkPolicyEnabled(android.webkit.WebSettings);
- method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDark(android.webkit.WebSettings);
- method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDarkStrategy(android.webkit.WebSettings);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getOffscreenPreRaster(android.webkit.WebSettings);
- method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList(android.webkit.WebSettings);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getSafeBrowsingEnabled(android.webkit.WebSettings);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isAlgorithmicDarkeningAllowed(android.webkit.WebSettings);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setAlgorithmicDarkeningAllowed(android.webkit.WebSettings, boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setDisabledActionModeMenuItems(android.webkit.WebSettings, int);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setEnterpriseAuthenticationAppLinkPolicyEnabled(android.webkit.WebSettings, boolean);
- method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDark(android.webkit.WebSettings, int);
- method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDarkStrategy(android.webkit.WebSettings, int);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setOffscreenPreRaster(android.webkit.WebSettings, boolean);
- method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setRequestedWithHeaderOriginAllowList(android.webkit.WebSettings, java.util.Set<java.lang.String!>);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingEnabled(android.webkit.WebSettings, boolean);
- field @Deprecated public static final int DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING = 2; // 0x2
- field @Deprecated public static final int DARK_STRATEGY_USER_AGENT_DARKENING_ONLY = 0; // 0x0
- field @Deprecated public static final int DARK_STRATEGY_WEB_THEME_DARKENING_ONLY = 1; // 0x1
- field @Deprecated public static final int FORCE_DARK_AUTO = 1; // 0x1
- field @Deprecated public static final int FORCE_DARK_OFF = 0; // 0x0
- field @Deprecated public static final int FORCE_DARK_ON = 2; // 0x2
- }
-
- public final class WebViewAssetLoader {
- method @WorkerThread public android.webkit.WebResourceResponse? shouldInterceptRequest(android.net.Uri);
- field public static final String DEFAULT_DOMAIN = "appassets.androidplatform.net";
- }
-
- public static final class WebViewAssetLoader.AssetsPathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
- ctor public WebViewAssetLoader.AssetsPathHandler(android.content.Context);
- method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
- }
-
- public static final class WebViewAssetLoader.Builder {
- ctor public WebViewAssetLoader.Builder();
- method public androidx.webkit.WebViewAssetLoader.Builder addPathHandler(String, androidx.webkit.WebViewAssetLoader.PathHandler);
- method public androidx.webkit.WebViewAssetLoader build();
- method public androidx.webkit.WebViewAssetLoader.Builder setDomain(String);
- method public androidx.webkit.WebViewAssetLoader.Builder setHttpAllowed(boolean);
- }
-
- public static final class WebViewAssetLoader.InternalStoragePathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
- ctor public WebViewAssetLoader.InternalStoragePathHandler(android.content.Context, java.io.File);
- method @WorkerThread public android.webkit.WebResourceResponse handle(String);
- }
-
- public static interface WebViewAssetLoader.PathHandler {
- method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
- }
-
- public static final class WebViewAssetLoader.ResourcesPathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
- ctor public WebViewAssetLoader.ResourcesPathHandler(android.content.Context);
- method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
- }
-
- public class WebViewClientCompat extends android.webkit.WebViewClient {
- ctor public WebViewClientCompat();
- method @RequiresApi(23) public final void onReceivedError(android.webkit.WebView, android.webkit.WebResourceRequest, android.webkit.WebResourceError);
- method @RequiresApi(21) @UiThread public void onReceivedError(android.webkit.WebView, android.webkit.WebResourceRequest, androidx.webkit.WebResourceErrorCompat);
- method @RequiresApi(27) public final void onSafeBrowsingHit(android.webkit.WebView, android.webkit.WebResourceRequest, int, android.webkit.SafeBrowsingResponse);
- method @UiThread public void onSafeBrowsingHit(android.webkit.WebView, android.webkit.WebResourceRequest, int, androidx.webkit.SafeBrowsingResponseCompat);
- }
-
- public class WebViewCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void addWebMessageListener(android.webkit.WebView, String, java.util.Set<java.lang.String!>, androidx.webkit.WebViewCompat.WebMessageListener);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebMessagePortCompat![] createWebMessageChannel(android.webkit.WebView);
- method public static android.content.pm.PackageInfo? getCurrentWebViewPackage(android.content.Context);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.net.Uri getSafeBrowsingPrivacyPolicyUrl();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_VARIATIONS_HEADER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static String getVariationsHeader();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_CHROME_CLIENT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.webkit.WebChromeClient? getWebChromeClient(android.webkit.WebView);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_VIEW_CLIENT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.webkit.WebViewClient getWebViewClient(android.webkit.WebView);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_VIEW_RENDERER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebViewRenderProcess? getWebViewRenderProcess(android.webkit.WebView);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebViewRenderProcessClient? getWebViewRenderProcessClient(android.webkit.WebView);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isMultiProcessEnabled();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.VISUAL_STATE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void postVisualStateCallback(android.webkit.WebView, long, androidx.webkit.WebViewCompat.VisualStateCallback);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.POST_WEB_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void postWebMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void removeWebMessageListener(android.webkit.WebView, String);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ALLOWLIST, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingAllowlist(java.util.Set<java.lang.String!>, android.webkit.ValueCallback<java.lang.Boolean!>?);
- method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_WHITELIST, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingWhitelist(java.util.List<java.lang.String!>, android.webkit.ValueCallback<java.lang.Boolean!>?);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setWebViewRenderProcessClient(android.webkit.WebView, java.util.concurrent.Executor, androidx.webkit.WebViewRenderProcessClient);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setWebViewRenderProcessClient(android.webkit.WebView, androidx.webkit.WebViewRenderProcessClient?);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.START_SAFE_BROWSING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void startSafeBrowsing(android.content.Context, android.webkit.ValueCallback<java.lang.Boolean!>?);
- }
-
- public static interface WebViewCompat.VisualStateCallback {
- method @UiThread public void onComplete(long);
- }
-
- public static interface WebViewCompat.WebMessageListener {
- method @UiThread public void onPostMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri, boolean, androidx.webkit.JavaScriptReplyProxy);
- }
-
- public class WebViewFeature {
- method public static boolean isFeatureSupported(String);
- method public static boolean isStartupFeatureSupported(android.content.Context, String);
- field public static final String ALGORITHMIC_DARKENING = "ALGORITHMIC_DARKENING";
- field public static final String CREATE_WEB_MESSAGE_CHANNEL = "CREATE_WEB_MESSAGE_CHANNEL";
- field public static final String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
- field public static final String ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY = "ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY";
- field public static final String FORCE_DARK = "FORCE_DARK";
- field public static final String FORCE_DARK_STRATEGY = "FORCE_DARK_STRATEGY";
- field public static final String GET_COOKIE_INFO = "GET_COOKIE_INFO";
- field public static final String GET_VARIATIONS_HEADER = "GET_VARIATIONS_HEADER";
- field public static final String GET_WEB_CHROME_CLIENT = "GET_WEB_CHROME_CLIENT";
- field public static final String GET_WEB_VIEW_CLIENT = "GET_WEB_VIEW_CLIENT";
- field public static final String GET_WEB_VIEW_RENDERER = "GET_WEB_VIEW_RENDERER";
- field public static final String MULTI_PROCESS = "MULTI_PROCESS";
- field public static final String OFF_SCREEN_PRERASTER = "OFF_SCREEN_PRERASTER";
- field public static final String POST_WEB_MESSAGE = "POST_WEB_MESSAGE";
- field public static final String PROXY_OVERRIDE = "PROXY_OVERRIDE";
- field public static final String PROXY_OVERRIDE_REVERSE_BYPASS = "PROXY_OVERRIDE_REVERSE_BYPASS";
- field public static final String RECEIVE_HTTP_ERROR = "RECEIVE_HTTP_ERROR";
- field public static final String RECEIVE_WEB_RESOURCE_ERROR = "RECEIVE_WEB_RESOURCE_ERROR";
- field public static final String SAFE_BROWSING_ALLOWLIST = "SAFE_BROWSING_ALLOWLIST";
- field public static final String SAFE_BROWSING_ENABLE = "SAFE_BROWSING_ENABLE";
- field public static final String SAFE_BROWSING_HIT = "SAFE_BROWSING_HIT";
- field public static final String SAFE_BROWSING_PRIVACY_POLICY_URL = "SAFE_BROWSING_PRIVACY_POLICY_URL";
- field public static final String SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY = "SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY";
- field public static final String SAFE_BROWSING_RESPONSE_PROCEED = "SAFE_BROWSING_RESPONSE_PROCEED";
- field public static final String SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL = "SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL";
- field @Deprecated public static final String SAFE_BROWSING_WHITELIST = "SAFE_BROWSING_WHITELIST";
- field public static final String SERVICE_WORKER_BASIC_USAGE = "SERVICE_WORKER_BASIC_USAGE";
- field public static final String SERVICE_WORKER_BLOCK_NETWORK_LOADS = "SERVICE_WORKER_BLOCK_NETWORK_LOADS";
- field public static final String SERVICE_WORKER_CACHE_MODE = "SERVICE_WORKER_CACHE_MODE";
- field public static final String SERVICE_WORKER_CONTENT_ACCESS = "SERVICE_WORKER_CONTENT_ACCESS";
- field public static final String SERVICE_WORKER_FILE_ACCESS = "SERVICE_WORKER_FILE_ACCESS";
- field public static final String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = "SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST";
- field public static final String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
- field public static final String STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX = "STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX";
- field public static final String START_SAFE_BROWSING = "START_SAFE_BROWSING";
- field public static final String TRACING_CONTROLLER_BASIC_USAGE = "TRACING_CONTROLLER_BASIC_USAGE";
- field public static final String VISUAL_STATE_CALLBACK = "VISUAL_STATE_CALLBACK";
- field public static final String WEB_MESSAGE_CALLBACK_ON_MESSAGE = "WEB_MESSAGE_CALLBACK_ON_MESSAGE";
- field public static final String WEB_MESSAGE_LISTENER = "WEB_MESSAGE_LISTENER";
- field public static final String WEB_MESSAGE_PORT_CLOSE = "WEB_MESSAGE_PORT_CLOSE";
- field public static final String WEB_MESSAGE_PORT_POST_MESSAGE = "WEB_MESSAGE_PORT_POST_MESSAGE";
- field public static final String WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK = "WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK";
- field public static final String WEB_RESOURCE_ERROR_GET_CODE = "WEB_RESOURCE_ERROR_GET_CODE";
- field public static final String WEB_RESOURCE_ERROR_GET_DESCRIPTION = "WEB_RESOURCE_ERROR_GET_DESCRIPTION";
- field public static final String WEB_RESOURCE_REQUEST_IS_REDIRECT = "WEB_RESOURCE_REQUEST_IS_REDIRECT";
- field public static final String WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE = "WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE";
- field public static final String WEB_VIEW_RENDERER_TERMINATE = "WEB_VIEW_RENDERER_TERMINATE";
- }
-
- public abstract class WebViewRenderProcess {
- ctor public WebViewRenderProcess();
- method public abstract boolean terminate();
- }
-
- public abstract class WebViewRenderProcessClient {
- ctor public WebViewRenderProcessClient();
- method public abstract void onRenderProcessResponsive(android.webkit.WebView, androidx.webkit.WebViewRenderProcess?);
- method public abstract void onRenderProcessUnresponsive(android.webkit.WebView, androidx.webkit.WebViewRenderProcess?);
- }
-
-}
-
diff --git a/webkit/webkit/api/restricted_1.6.0-beta02.txt b/webkit/webkit/api/restricted_1.6.0-beta02.txt
deleted file mode 100644
index faf13cb..0000000
--- a/webkit/webkit/api/restricted_1.6.0-beta02.txt
+++ /dev/null
@@ -1,300 +0,0 @@
-// Signature format: 4.0
-package androidx.webkit {
-
- public class CookieManagerCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_COOKIE_INFO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.List<java.lang.String!> getCookieInfo(android.webkit.CookieManager, String);
- }
-
- public abstract class JavaScriptReplyProxy {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
- }
-
- public class ProcessGlobalConfig {
- ctor public ProcessGlobalConfig();
- method public static void apply(androidx.webkit.ProcessGlobalConfig);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
- }
-
- public final class ProxyConfig {
- method public java.util.List<java.lang.String!> getBypassRules();
- method public java.util.List<androidx.webkit.ProxyConfig.ProxyRule!> getProxyRules();
- method public boolean isReverseBypassEnabled();
- field public static final String MATCH_ALL_SCHEMES = "*";
- field public static final String MATCH_HTTP = "http";
- field public static final String MATCH_HTTPS = "https";
- }
-
- public static final class ProxyConfig.Builder {
- ctor public ProxyConfig.Builder();
- ctor public ProxyConfig.Builder(androidx.webkit.ProxyConfig);
- method public androidx.webkit.ProxyConfig.Builder addBypassRule(String);
- method public androidx.webkit.ProxyConfig.Builder addDirect(String);
- method public androidx.webkit.ProxyConfig.Builder addDirect();
- method public androidx.webkit.ProxyConfig.Builder addProxyRule(String);
- method public androidx.webkit.ProxyConfig.Builder addProxyRule(String, String);
- method public androidx.webkit.ProxyConfig build();
- method public androidx.webkit.ProxyConfig.Builder bypassSimpleHostnames();
- method public androidx.webkit.ProxyConfig.Builder removeImplicitRules();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.PROXY_OVERRIDE_REVERSE_BYPASS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public androidx.webkit.ProxyConfig.Builder setReverseBypassEnabled(boolean);
- }
-
- public static final class ProxyConfig.ProxyRule {
- method public String getSchemeFilter();
- method public String getUrl();
- }
-
- public abstract class ProxyController {
- method public abstract void clearProxyOverride(java.util.concurrent.Executor, Runnable);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.PROXY_OVERRIDE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.ProxyController getInstance();
- method public abstract void setProxyOverride(androidx.webkit.ProxyConfig, java.util.concurrent.Executor, Runnable);
- }
-
- public abstract class SafeBrowsingResponseCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void backToSafety(boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void proceed(boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void showInterstitial(boolean);
- }
-
- public abstract class ServiceWorkerClientCompat {
- ctor public ServiceWorkerClientCompat();
- method @WorkerThread public abstract android.webkit.WebResourceResponse? shouldInterceptRequest(android.webkit.WebResourceRequest);
- }
-
- public abstract class ServiceWorkerControllerCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.ServiceWorkerControllerCompat getInstance();
- method public abstract androidx.webkit.ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings();
- method public abstract void setServiceWorkerClient(androidx.webkit.ServiceWorkerClientCompat?);
- }
-
- public abstract class ServiceWorkerWebSettingsCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowContentAccess();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowFileAccess();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getBlockNetworkLoads();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getCacheMode();
- method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowContentAccess(boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowFileAccess(boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setBlockNetworkLoads(boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setCacheMode(int);
- method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String!>);
- }
-
- public class TracingConfig {
- method public java.util.List<java.lang.String!> getCustomIncludedCategories();
- method public int getPredefinedCategories();
- method public int getTracingMode();
- field public static final int CATEGORIES_ALL = 1; // 0x1
- field public static final int CATEGORIES_ANDROID_WEBVIEW = 2; // 0x2
- field public static final int CATEGORIES_FRAME_VIEWER = 64; // 0x40
- field public static final int CATEGORIES_INPUT_LATENCY = 8; // 0x8
- field public static final int CATEGORIES_JAVASCRIPT_AND_RENDERING = 32; // 0x20
- field public static final int CATEGORIES_NONE = 0; // 0x0
- field public static final int CATEGORIES_RENDERING = 16; // 0x10
- field public static final int CATEGORIES_WEB_DEVELOPER = 4; // 0x4
- field public static final int RECORD_CONTINUOUSLY = 1; // 0x1
- field public static final int RECORD_UNTIL_FULL = 0; // 0x0
- }
-
- public static class TracingConfig.Builder {
- ctor public TracingConfig.Builder();
- method public androidx.webkit.TracingConfig.Builder addCategories(int...);
- method public androidx.webkit.TracingConfig.Builder addCategories(java.lang.String!...);
- method public androidx.webkit.TracingConfig.Builder addCategories(java.util.Collection<java.lang.String!>);
- method public androidx.webkit.TracingConfig build();
- method public androidx.webkit.TracingConfig.Builder setTracingMode(int);
- }
-
- public abstract class TracingController {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.TRACING_CONTROLLER_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.TracingController getInstance();
- method public abstract boolean isTracing();
- method public abstract void start(androidx.webkit.TracingConfig);
- method public abstract boolean stop(java.io.OutputStream?, java.util.concurrent.Executor);
- }
-
- public class WebMessageCompat {
- ctor public WebMessageCompat(String?);
- ctor public WebMessageCompat(String?, androidx.webkit.WebMessagePortCompat![]?);
- method public String? getData();
- method public androidx.webkit.WebMessagePortCompat![]? getPorts();
- }
-
- public abstract class WebMessagePortCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_CLOSE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void close();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_POST_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(androidx.webkit.WebMessageCompat);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setWebMessageCallback(androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setWebMessageCallback(android.os.Handler?, androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat);
- }
-
- public abstract static class WebMessagePortCompat.WebMessageCallbackCompat {
- ctor public WebMessagePortCompat.WebMessageCallbackCompat();
- method public void onMessage(androidx.webkit.WebMessagePortCompat, androidx.webkit.WebMessageCompat?);
- }
-
- public abstract class WebResourceErrorCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract CharSequence getDescription();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getErrorCode();
- }
-
- public class WebResourceRequestCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isRedirect(android.webkit.WebResourceRequest);
- }
-
- public class WebSettingsCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getDisabledActionModeMenuItems(android.webkit.WebSettings);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getEnterpriseAuthenticationAppLinkPolicyEnabled(android.webkit.WebSettings);
- method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDark(android.webkit.WebSettings);
- method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDarkStrategy(android.webkit.WebSettings);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getOffscreenPreRaster(android.webkit.WebSettings);
- method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList(android.webkit.WebSettings);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getSafeBrowsingEnabled(android.webkit.WebSettings);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isAlgorithmicDarkeningAllowed(android.webkit.WebSettings);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setAlgorithmicDarkeningAllowed(android.webkit.WebSettings, boolean);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setDisabledActionModeMenuItems(android.webkit.WebSettings, int);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setEnterpriseAuthenticationAppLinkPolicyEnabled(android.webkit.WebSettings, boolean);
- method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDark(android.webkit.WebSettings, int);
- method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDarkStrategy(android.webkit.WebSettings, int);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setOffscreenPreRaster(android.webkit.WebSettings, boolean);
- method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setRequestedWithHeaderOriginAllowList(android.webkit.WebSettings, java.util.Set<java.lang.String!>);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingEnabled(android.webkit.WebSettings, boolean);
- field @Deprecated public static final int DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING = 2; // 0x2
- field @Deprecated public static final int DARK_STRATEGY_USER_AGENT_DARKENING_ONLY = 0; // 0x0
- field @Deprecated public static final int DARK_STRATEGY_WEB_THEME_DARKENING_ONLY = 1; // 0x1
- field @Deprecated public static final int FORCE_DARK_AUTO = 1; // 0x1
- field @Deprecated public static final int FORCE_DARK_OFF = 0; // 0x0
- field @Deprecated public static final int FORCE_DARK_ON = 2; // 0x2
- }
-
- public final class WebViewAssetLoader {
- method @WorkerThread public android.webkit.WebResourceResponse? shouldInterceptRequest(android.net.Uri);
- field public static final String DEFAULT_DOMAIN = "appassets.androidplatform.net";
- }
-
- public static final class WebViewAssetLoader.AssetsPathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
- ctor public WebViewAssetLoader.AssetsPathHandler(android.content.Context);
- method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
- }
-
- public static final class WebViewAssetLoader.Builder {
- ctor public WebViewAssetLoader.Builder();
- method public androidx.webkit.WebViewAssetLoader.Builder addPathHandler(String, androidx.webkit.WebViewAssetLoader.PathHandler);
- method public androidx.webkit.WebViewAssetLoader build();
- method public androidx.webkit.WebViewAssetLoader.Builder setDomain(String);
- method public androidx.webkit.WebViewAssetLoader.Builder setHttpAllowed(boolean);
- }
-
- public static final class WebViewAssetLoader.InternalStoragePathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
- ctor public WebViewAssetLoader.InternalStoragePathHandler(android.content.Context, java.io.File);
- method @WorkerThread public android.webkit.WebResourceResponse handle(String);
- }
-
- public static interface WebViewAssetLoader.PathHandler {
- method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
- }
-
- public static final class WebViewAssetLoader.ResourcesPathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
- ctor public WebViewAssetLoader.ResourcesPathHandler(android.content.Context);
- method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
- }
-
- public class WebViewClientCompat extends android.webkit.WebViewClient {
- ctor public WebViewClientCompat();
- method @RequiresApi(23) public final void onReceivedError(android.webkit.WebView, android.webkit.WebResourceRequest, android.webkit.WebResourceError);
- method @RequiresApi(21) @UiThread public void onReceivedError(android.webkit.WebView, android.webkit.WebResourceRequest, androidx.webkit.WebResourceErrorCompat);
- method @RequiresApi(27) public final void onSafeBrowsingHit(android.webkit.WebView, android.webkit.WebResourceRequest, int, android.webkit.SafeBrowsingResponse);
- method @UiThread public void onSafeBrowsingHit(android.webkit.WebView, android.webkit.WebResourceRequest, int, androidx.webkit.SafeBrowsingResponseCompat);
- }
-
- public class WebViewCompat {
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void addWebMessageListener(android.webkit.WebView, String, java.util.Set<java.lang.String!>, androidx.webkit.WebViewCompat.WebMessageListener);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebMessagePortCompat![] createWebMessageChannel(android.webkit.WebView);
- method public static android.content.pm.PackageInfo? getCurrentWebViewPackage(android.content.Context);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.net.Uri getSafeBrowsingPrivacyPolicyUrl();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_VARIATIONS_HEADER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static String getVariationsHeader();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_CHROME_CLIENT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.webkit.WebChromeClient? getWebChromeClient(android.webkit.WebView);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_VIEW_CLIENT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.webkit.WebViewClient getWebViewClient(android.webkit.WebView);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_VIEW_RENDERER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebViewRenderProcess? getWebViewRenderProcess(android.webkit.WebView);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebViewRenderProcessClient? getWebViewRenderProcessClient(android.webkit.WebView);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isMultiProcessEnabled();
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.VISUAL_STATE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void postVisualStateCallback(android.webkit.WebView, long, androidx.webkit.WebViewCompat.VisualStateCallback);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.POST_WEB_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void postWebMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void removeWebMessageListener(android.webkit.WebView, String);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ALLOWLIST, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingAllowlist(java.util.Set<java.lang.String!>, android.webkit.ValueCallback<java.lang.Boolean!>?);
- method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_WHITELIST, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingWhitelist(java.util.List<java.lang.String!>, android.webkit.ValueCallback<java.lang.Boolean!>?);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setWebViewRenderProcessClient(android.webkit.WebView, java.util.concurrent.Executor, androidx.webkit.WebViewRenderProcessClient);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setWebViewRenderProcessClient(android.webkit.WebView, androidx.webkit.WebViewRenderProcessClient?);
- method @RequiresFeature(name=androidx.webkit.WebViewFeature.START_SAFE_BROWSING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void startSafeBrowsing(android.content.Context, android.webkit.ValueCallback<java.lang.Boolean!>?);
- }
-
- public static interface WebViewCompat.VisualStateCallback {
- method @UiThread public void onComplete(long);
- }
-
- public static interface WebViewCompat.WebMessageListener {
- method @UiThread public void onPostMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri, boolean, androidx.webkit.JavaScriptReplyProxy);
- }
-
- public class WebViewFeature {
- method public static boolean isFeatureSupported(String);
- method public static boolean isStartupFeatureSupported(android.content.Context, String);
- field public static final String ALGORITHMIC_DARKENING = "ALGORITHMIC_DARKENING";
- field public static final String CREATE_WEB_MESSAGE_CHANNEL = "CREATE_WEB_MESSAGE_CHANNEL";
- field public static final String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
- field public static final String ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY = "ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY";
- field public static final String FORCE_DARK = "FORCE_DARK";
- field public static final String FORCE_DARK_STRATEGY = "FORCE_DARK_STRATEGY";
- field public static final String GET_COOKIE_INFO = "GET_COOKIE_INFO";
- field public static final String GET_VARIATIONS_HEADER = "GET_VARIATIONS_HEADER";
- field public static final String GET_WEB_CHROME_CLIENT = "GET_WEB_CHROME_CLIENT";
- field public static final String GET_WEB_VIEW_CLIENT = "GET_WEB_VIEW_CLIENT";
- field public static final String GET_WEB_VIEW_RENDERER = "GET_WEB_VIEW_RENDERER";
- field public static final String MULTI_PROCESS = "MULTI_PROCESS";
- field public static final String OFF_SCREEN_PRERASTER = "OFF_SCREEN_PRERASTER";
- field public static final String POST_WEB_MESSAGE = "POST_WEB_MESSAGE";
- field public static final String PROXY_OVERRIDE = "PROXY_OVERRIDE";
- field public static final String PROXY_OVERRIDE_REVERSE_BYPASS = "PROXY_OVERRIDE_REVERSE_BYPASS";
- field public static final String RECEIVE_HTTP_ERROR = "RECEIVE_HTTP_ERROR";
- field public static final String RECEIVE_WEB_RESOURCE_ERROR = "RECEIVE_WEB_RESOURCE_ERROR";
- field public static final String SAFE_BROWSING_ALLOWLIST = "SAFE_BROWSING_ALLOWLIST";
- field public static final String SAFE_BROWSING_ENABLE = "SAFE_BROWSING_ENABLE";
- field public static final String SAFE_BROWSING_HIT = "SAFE_BROWSING_HIT";
- field public static final String SAFE_BROWSING_PRIVACY_POLICY_URL = "SAFE_BROWSING_PRIVACY_POLICY_URL";
- field public static final String SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY = "SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY";
- field public static final String SAFE_BROWSING_RESPONSE_PROCEED = "SAFE_BROWSING_RESPONSE_PROCEED";
- field public static final String SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL = "SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL";
- field @Deprecated public static final String SAFE_BROWSING_WHITELIST = "SAFE_BROWSING_WHITELIST";
- field public static final String SERVICE_WORKER_BASIC_USAGE = "SERVICE_WORKER_BASIC_USAGE";
- field public static final String SERVICE_WORKER_BLOCK_NETWORK_LOADS = "SERVICE_WORKER_BLOCK_NETWORK_LOADS";
- field public static final String SERVICE_WORKER_CACHE_MODE = "SERVICE_WORKER_CACHE_MODE";
- field public static final String SERVICE_WORKER_CONTENT_ACCESS = "SERVICE_WORKER_CONTENT_ACCESS";
- field public static final String SERVICE_WORKER_FILE_ACCESS = "SERVICE_WORKER_FILE_ACCESS";
- field public static final String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = "SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST";
- field public static final String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
- field public static final String STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX = "STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX";
- field public static final String START_SAFE_BROWSING = "START_SAFE_BROWSING";
- field public static final String TRACING_CONTROLLER_BASIC_USAGE = "TRACING_CONTROLLER_BASIC_USAGE";
- field public static final String VISUAL_STATE_CALLBACK = "VISUAL_STATE_CALLBACK";
- field public static final String WEB_MESSAGE_CALLBACK_ON_MESSAGE = "WEB_MESSAGE_CALLBACK_ON_MESSAGE";
- field public static final String WEB_MESSAGE_LISTENER = "WEB_MESSAGE_LISTENER";
- field public static final String WEB_MESSAGE_PORT_CLOSE = "WEB_MESSAGE_PORT_CLOSE";
- field public static final String WEB_MESSAGE_PORT_POST_MESSAGE = "WEB_MESSAGE_PORT_POST_MESSAGE";
- field public static final String WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK = "WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK";
- field public static final String WEB_RESOURCE_ERROR_GET_CODE = "WEB_RESOURCE_ERROR_GET_CODE";
- field public static final String WEB_RESOURCE_ERROR_GET_DESCRIPTION = "WEB_RESOURCE_ERROR_GET_DESCRIPTION";
- field public static final String WEB_RESOURCE_REQUEST_IS_REDIRECT = "WEB_RESOURCE_REQUEST_IS_REDIRECT";
- field public static final String WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE = "WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE";
- field public static final String WEB_VIEW_RENDERER_TERMINATE = "WEB_VIEW_RENDERER_TERMINATE";
- }
-
- public abstract class WebViewRenderProcess {
- ctor public WebViewRenderProcess();
- method public abstract boolean terminate();
- }
-
- public abstract class WebViewRenderProcessClient {
- ctor public WebViewRenderProcessClient();
- method public abstract void onRenderProcessResponsive(android.webkit.WebView, androidx.webkit.WebViewRenderProcess?);
- method public abstract void onRenderProcessUnresponsive(android.webkit.WebView, androidx.webkit.WebViewRenderProcess?);
- }
-
-}
-
diff --git a/webkit/webkit/build.gradle b/webkit/webkit/build.gradle
index 0cc7c77..446ce33 100644
--- a/webkit/webkit/build.gradle
+++ b/webkit/webkit/build.gradle
@@ -68,9 +68,9 @@
}
androidx {
- name = "WebView Support Library"
+ name = "WebKit"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
- description = "The WebView Support Library is a static library you can add to your Android application in order to use android.webkit APIs that are not available for older platform versions."
+ description = "The WebKit Support Library is a static library you can add to your Android application in order to use android.webkit APIs that are not available for older platform versions."
additionalDeviceTestApkKeys.add("chrome")
}
diff --git a/window/extensions/core/core/build.gradle b/window/extensions/core/core/build.gradle
index 9104d8c..5a895b0 100644
--- a/window/extensions/core/core/build.gradle
+++ b/window/extensions/core/core/build.gradle
@@ -45,7 +45,7 @@
}
androidx {
- name = "Jetpack WindowManager library Core Extensions"
+ name = "WindowManager Core Extensions"
type = LibraryType.PUBLISHED_LIBRARY
publish = Publish.SNAPSHOT_AND_RELEASE // Only to generate per-project-zips
inceptionYear = "2022"
diff --git a/window/extensions/extensions/api/current.txt b/window/extensions/extensions/api/current.txt
index bf580d9..9cedeaf 100644
--- a/window/extensions/extensions/api/current.txt
+++ b/window/extensions/extensions/api/current.txt
@@ -16,13 +16,31 @@
package androidx.window.extensions.area {
+ public interface ExtensionWindowAreaPresentation {
+ method public android.content.Context getPresentationContext();
+ method public void setPresentationView(android.view.View);
+ }
+
+ public interface ExtensionWindowAreaStatus {
+ method public android.util.DisplayMetrics getWindowAreaDisplayMetrics();
+ method public int getWindowAreaStatus();
+ }
+
public interface WindowAreaComponent {
+ method public default void addRearDisplayPresentationStatusListener(androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.area.ExtensionWindowAreaStatus!>);
method public void addRearDisplayStatusListener(androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
+ method public default void endRearDisplayPresentationSession();
method public void endRearDisplaySession();
+ method public default androidx.window.extensions.area.ExtensionWindowAreaPresentation? getRearDisplayPresentation();
+ method public default void removeRearDisplayPresentationStatusListener(androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.area.ExtensionWindowAreaStatus!>);
method public void removeRearDisplayStatusListener(androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
+ method public default void startRearDisplayPresentationSession(android.app.Activity, androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
method public void startRearDisplaySession(android.app.Activity, androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
field public static final int SESSION_STATE_ACTIVE = 1; // 0x1
+ field public static final int SESSION_STATE_CONTENT_INVISIBLE = 3; // 0x3
+ field public static final int SESSION_STATE_CONTENT_VISIBLE = 2; // 0x2
field public static final int SESSION_STATE_INACTIVE = 0; // 0x0
+ field public static final int STATUS_ACTIVE = 3; // 0x3
field public static final int STATUS_AVAILABLE = 2; // 0x2
field public static final int STATUS_UNAVAILABLE = 1; // 0x1
field public static final int STATUS_UNSUPPORTED = 0; // 0x0
@@ -35,11 +53,15 @@
public interface ActivityEmbeddingComponent {
method public void clearSplitAttributesCalculator();
method public void clearSplitInfoCallback();
+ method public default void finishActivityStacks(java.util.Set<android.os.IBinder!>);
+ method public default void invalidateTopVisibleSplitAttributes();
method public boolean isActivityEmbedded(android.app.Activity);
method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
+ method 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!>);
method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
method public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
+ method public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);
}
public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
@@ -117,6 +139,7 @@
method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
method public androidx.window.extensions.embedding.SplitAttributes getSplitAttributes();
method @Deprecated public float getSplitRatio();
+ method public android.os.IBinder getToken();
}
public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
diff --git a/window/extensions/extensions/api/public_plus_experimental_current.txt b/window/extensions/extensions/api/public_plus_experimental_current.txt
index bf580d9..9cedeaf 100644
--- a/window/extensions/extensions/api/public_plus_experimental_current.txt
+++ b/window/extensions/extensions/api/public_plus_experimental_current.txt
@@ -16,13 +16,31 @@
package androidx.window.extensions.area {
+ public interface ExtensionWindowAreaPresentation {
+ method public android.content.Context getPresentationContext();
+ method public void setPresentationView(android.view.View);
+ }
+
+ public interface ExtensionWindowAreaStatus {
+ method public android.util.DisplayMetrics getWindowAreaDisplayMetrics();
+ method public int getWindowAreaStatus();
+ }
+
public interface WindowAreaComponent {
+ method public default void addRearDisplayPresentationStatusListener(androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.area.ExtensionWindowAreaStatus!>);
method public void addRearDisplayStatusListener(androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
+ method public default void endRearDisplayPresentationSession();
method public void endRearDisplaySession();
+ method public default androidx.window.extensions.area.ExtensionWindowAreaPresentation? getRearDisplayPresentation();
+ method public default void removeRearDisplayPresentationStatusListener(androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.area.ExtensionWindowAreaStatus!>);
method public void removeRearDisplayStatusListener(androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
+ method public default void startRearDisplayPresentationSession(android.app.Activity, androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
method public void startRearDisplaySession(android.app.Activity, androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
field public static final int SESSION_STATE_ACTIVE = 1; // 0x1
+ field public static final int SESSION_STATE_CONTENT_INVISIBLE = 3; // 0x3
+ field public static final int SESSION_STATE_CONTENT_VISIBLE = 2; // 0x2
field public static final int SESSION_STATE_INACTIVE = 0; // 0x0
+ field public static final int STATUS_ACTIVE = 3; // 0x3
field public static final int STATUS_AVAILABLE = 2; // 0x2
field public static final int STATUS_UNAVAILABLE = 1; // 0x1
field public static final int STATUS_UNSUPPORTED = 0; // 0x0
@@ -35,11 +53,15 @@
public interface ActivityEmbeddingComponent {
method public void clearSplitAttributesCalculator();
method public void clearSplitInfoCallback();
+ method public default void finishActivityStacks(java.util.Set<android.os.IBinder!>);
+ method public default void invalidateTopVisibleSplitAttributes();
method public boolean isActivityEmbedded(android.app.Activity);
method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
+ method 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!>);
method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
method public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
+ method public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);
}
public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
@@ -117,6 +139,7 @@
method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
method public androidx.window.extensions.embedding.SplitAttributes getSplitAttributes();
method @Deprecated public float getSplitRatio();
+ method public android.os.IBinder getToken();
}
public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
diff --git a/window/extensions/extensions/api/restricted_current.txt b/window/extensions/extensions/api/restricted_current.txt
index bf580d9..9cedeaf 100644
--- a/window/extensions/extensions/api/restricted_current.txt
+++ b/window/extensions/extensions/api/restricted_current.txt
@@ -16,13 +16,31 @@
package androidx.window.extensions.area {
+ public interface ExtensionWindowAreaPresentation {
+ method public android.content.Context getPresentationContext();
+ method public void setPresentationView(android.view.View);
+ }
+
+ public interface ExtensionWindowAreaStatus {
+ method public android.util.DisplayMetrics getWindowAreaDisplayMetrics();
+ method public int getWindowAreaStatus();
+ }
+
public interface WindowAreaComponent {
+ method public default void addRearDisplayPresentationStatusListener(androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.area.ExtensionWindowAreaStatus!>);
method public void addRearDisplayStatusListener(androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
+ method public default void endRearDisplayPresentationSession();
method public void endRearDisplaySession();
+ method public default androidx.window.extensions.area.ExtensionWindowAreaPresentation? getRearDisplayPresentation();
+ method public default void removeRearDisplayPresentationStatusListener(androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.area.ExtensionWindowAreaStatus!>);
method public void removeRearDisplayStatusListener(androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
+ method public default void startRearDisplayPresentationSession(android.app.Activity, androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
method public void startRearDisplaySession(android.app.Activity, androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
field public static final int SESSION_STATE_ACTIVE = 1; // 0x1
+ field public static final int SESSION_STATE_CONTENT_INVISIBLE = 3; // 0x3
+ field public static final int SESSION_STATE_CONTENT_VISIBLE = 2; // 0x2
field public static final int SESSION_STATE_INACTIVE = 0; // 0x0
+ field public static final int STATUS_ACTIVE = 3; // 0x3
field public static final int STATUS_AVAILABLE = 2; // 0x2
field public static final int STATUS_UNAVAILABLE = 1; // 0x1
field public static final int STATUS_UNSUPPORTED = 0; // 0x0
@@ -35,11 +53,15 @@
public interface ActivityEmbeddingComponent {
method public void clearSplitAttributesCalculator();
method public void clearSplitInfoCallback();
+ method public default void finishActivityStacks(java.util.Set<android.os.IBinder!>);
+ method public default void invalidateTopVisibleSplitAttributes();
method public boolean isActivityEmbedded(android.app.Activity);
method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
+ method 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!>);
method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
method public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
+ method public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);
}
public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
@@ -117,6 +139,7 @@
method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
method public androidx.window.extensions.embedding.SplitAttributes getSplitAttributes();
method @Deprecated public float getSplitRatio();
+ method public android.os.IBinder getToken();
}
public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
diff --git a/window/extensions/extensions/build.gradle b/window/extensions/extensions/build.gradle
index 279cbba..ef58960 100644
--- a/window/extensions/extensions/build.gradle
+++ b/window/extensions/extensions/build.gradle
@@ -26,7 +26,7 @@
api(libs.kotlinStdlib)
implementation("androidx.annotation:annotation:1.6.0")
implementation("androidx.annotation:annotation-experimental:1.1.0")
- implementation("androidx.window.extensions.core:core:1.0.0-rc01")
+ implementation("androidx.window.extensions.core:core:1.0.0-beta01")
testImplementation(libs.robolectric)
testImplementation(libs.testExtJunit)
@@ -41,7 +41,7 @@
}
androidx {
- name = "Jetpack WindowManager library Extensions"
+ name = "WindowManager Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE // Only to generate per-project-zips
runApiTasks = new RunApiTasks.Yes("Need to track API surface before moving to publish")
inceptionYear = "2020"
diff --git a/window/extensions/extensions/lint-baseline.xml b/window/extensions/extensions/lint-baseline.xml
index 1ab80d6..93cb09a 100644
--- a/window/extensions/extensions/lint-baseline.xml
+++ b/window/extensions/extensions/lint-baseline.xml
@@ -4,6 +4,24 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
+ errorLine1=" default void setSplitAttributesCalculator(@NonNull SplitAttributesCalculator calculator) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1="public interface SplitAttributesCalculator {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculator.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
errorLine1=" @interface WindowAreaStatus {}"
errorLine2=" ~~~~~~~~~~~~~~~~">
<location
@@ -22,6 +40,42 @@
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
+ errorLine1=" ArrayMap<java.util.function.Consumer<Integer>, Consumer<Integer>> JAVA_TO_EXTENSIONS_MAP ="
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/window/extensions/area/WindowAreaComponent.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" default void addRearDisplayStatusListener("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/window/extensions/area/WindowAreaComponent.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" default void removeRearDisplayStatusListener("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/window/extensions/area/WindowAreaComponent.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" default void startRearDisplaySession(@NonNull Activity activity,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/window/extensions/area/WindowAreaComponent.java"/>
+ </issue>
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
errorLine1=" int INVALID_VENDOR_API_LEVEL = -1;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -46,4 +100,13 @@
file="src/main/java/androidx/window/extensions/WindowExtensions.java"/>
</issue>
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1=" int VENDOR_API_LEVEL_3 = 3;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/window/extensions/WindowExtensions.java"/>
+ </issue>
+
</issues>
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
index 240e2dc..447e5c1 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
@@ -18,12 +18,20 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import android.app.ActivityOptions;
+import android.os.IBinder;
+
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.window.extensions.area.WindowAreaComponent;
import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
+import androidx.window.extensions.embedding.ActivityStack;
+import androidx.window.extensions.embedding.SplitAttributes;
+import androidx.window.extensions.embedding.SplitInfo;
import androidx.window.extensions.layout.WindowLayoutComponent;
+import java.util.Set;
+
/**
* A class to provide instances of different WindowManager Jetpack extension components. An OEM must
* implement all the availability methods to state which WindowManager Jetpack extension
@@ -56,6 +64,7 @@
* <li>{@link androidx.window.extensions.layout.FoldingFeature} APIs</li>
* <li>{@link androidx.window.extensions.layout.WindowLayoutInfo} APIs</li>
* <li>{@link androidx.window.extensions.layout.WindowLayoutComponent} APIs</li>
+ * <li>{@link androidx.window.extensions.area.WindowAreaComponent} APIs</li>
* </ul>
* </p>
* @hide
@@ -79,6 +88,28 @@
@RestrictTo(LIBRARY_GROUP)
int VENDOR_API_LEVEL_2 = 2;
+ // TODO(b/241323716) Removed after we have annotation to check API level
+ /**
+ * A vendor API level constant. It helps to unify the format of documenting {@code @since}
+ * block.
+ * <p>
+ * The added APIs for Vendor API level 3 are:
+ * <ul>
+ * <li>{@link ActivityStack#getToken()}</li>
+ * <li>{@link SplitInfo#getToken()}</li>
+ * <li>{@link ActivityEmbeddingComponent#setLaunchingActivityStack(ActivityOptions,
+ * IBinder)}</li>
+ * <li>{@link ActivityEmbeddingComponent#invalidateTopVisibleSplitAttributes()}</li>
+ * <li>{@link ActivityEmbeddingComponent#updateSplitAttributes(IBinder, SplitAttributes)}
+ * </li>
+ * <li>{@link ActivityEmbeddingComponent#finishActivityStacks(Set)}</li>
+ * </ul>
+ * </p>
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ int VENDOR_API_LEVEL_3 = 3;
+
/**
* Returns the API level of the vendor library on the device. If the returned version is not
* supported by the WindowManager library, then some functions may not be available or replaced
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/area/ExtensionWindowAreaPresentation.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/area/ExtensionWindowAreaPresentation.java
new file mode 100644
index 0000000..0ce24b8
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/area/ExtensionWindowAreaPresentation.java
@@ -0,0 +1,44 @@
+/*
+ * 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.area;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+/**
+ * An interface representing a container in an extension window area in which app content can be
+ * shown.
+ *
+ * Since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_3}
+ * @see WindowAreaComponent#getRearDisplayPresentation()
+ */
+public interface ExtensionWindowAreaPresentation {
+
+ /**
+ * Returns the {@link Context} for the window that is being used
+ * to display the additional content provided from the application.
+ */
+ @NonNull
+ Context getPresentationContext();
+
+ /**
+ * Sets the {@link View} that the application wants to display in the extension window area.
+ */
+ void setPresentationView(@NonNull View view);
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/area/ExtensionWindowAreaStatus.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/area/ExtensionWindowAreaStatus.java
new file mode 100644
index 0000000..0dcd47f
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/area/ExtensionWindowAreaStatus.java
@@ -0,0 +1,44 @@
+/*
+ * 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.area;
+
+import android.util.DisplayMetrics;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Interface to provide information around the current status of a window area feature.
+ *
+ * Since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_3}
+ * @see WindowAreaComponent#addRearDisplayPresentationStatusListener
+ */
+public interface ExtensionWindowAreaStatus {
+
+ /**
+ * Returns the {@link androidx.window.extensions.area.WindowAreaComponent.WindowAreaStatus}
+ * value that relates to the current status of a feature.
+ */
+ @WindowAreaComponent.WindowAreaStatus
+ int getWindowAreaStatus();
+
+ /**
+ * Returns the {@link DisplayMetrics} that corresponds to the window area that a feature
+ * interacts with. This is converted to size class information provided to developers.
+ */
+ @NonNull
+ DisplayMetrics getWindowAreaDisplayMetrics();
+}
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 422e972..f54f1f5 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,10 +16,15 @@
package androidx.window.extensions.area;
+import android.annotation.SuppressLint;
import android.app.Activity;
+import android.os.Build;
+import android.util.ArrayMap;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.window.extensions.WindowExtensions;
import androidx.window.extensions.core.util.function.Consumer;
@@ -45,6 +50,8 @@
* WindowArea status constant to signify that the feature is
* unsupported on this device. Could be due to the device not supporting that
* specific feature.
+ *
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_2}
*/
int STATUS_UNSUPPORTED = 0;
@@ -53,15 +60,27 @@
* currently unavailable but is supported on this device. This value could signify
* that the current device state does not support the specific feature or another
* process is currently enabled in that feature.
+ *
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_2}
*/
int STATUS_UNAVAILABLE = 1;
/**
* WindowArea status constant to signify that the feature is
* available to be entered or enabled.
+ *
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_2}
*/
int STATUS_AVAILABLE = 2;
+ /**
+ * WindowArea status constant to signify that the feature is
+ * already enabled.
+ *
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_3}
+ */
+ int STATUS_ACTIVE = 3;
+
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@@ -69,7 +88,8 @@
@IntDef({
STATUS_UNSUPPORTED,
STATUS_UNAVAILABLE,
- STATUS_AVAILABLE
+ STATUS_AVAILABLE,
+ STATUS_ACTIVE
})
@interface WindowAreaStatus {}
@@ -88,16 +108,40 @@
*/
int SESSION_STATE_ACTIVE = 1;
+ /**
+ * Session state constant to represent that there is an
+ * active presentation session currently in progress, and the content provided by the
+ * application is visible.
+ */
+ int SESSION_STATE_CONTENT_VISIBLE = 2;
+
+ /**
+ * Session state constant to represent that there is an
+ * active presentation session currently in progress, but the content provided by the
+ * application is no longer visible.
+ */
+ int SESSION_STATE_CONTENT_INVISIBLE = 3;
+
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@IntDef({
SESSION_STATE_ACTIVE,
- SESSION_STATE_INACTIVE
+ SESSION_STATE_INACTIVE,
+ SESSION_STATE_CONTENT_VISIBLE,
+ SESSION_STATE_CONTENT_INVISIBLE
})
@interface WindowAreaSessionState {}
+ // TODO(b/264546746): Remove deprecated Window Extensions APIs after apps in g3 is updated to
+ // the latest library.
+ /** @hide */
+ @SuppressLint({"NewApi", "ClassVerificationFailure"})
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ ArrayMap<java.util.function.Consumer<Integer>, Consumer<Integer>> JAVA_TO_EXTENSIONS_MAP =
+ new ArrayMap<>();
+
/**
* Adds a listener interested in receiving updates on the RearDisplayStatus
* of the device. Because this is being called from the OEM provided
@@ -108,21 +152,65 @@
* correspond to the [WindowAreaStatus] value that aligns with the current status
* of the rear display.
* @param consumer interested in receiving updates to WindowAreaStatus.
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_2}
*/
- void addRearDisplayStatusListener(@NonNull Consumer<@WindowAreaStatus Integer> consumer);
+ void addRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
+
+ // TODO(b/264546746): Remove deprecated Window Extensions APIs after apps in g3 is updated to
+ // the latest library.
+ /**
+ * @deprecated Use {@link #addRearDisplayStatusListener(Consumer)}.
+ *
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_2}
+ * @hide
+ */
+ @Deprecated
+ @SuppressLint("ClassVerificationFailure")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ default void addRearDisplayStatusListener(
+ @NonNull java.util.function.Consumer<Integer> consumer) {
+ if (JAVA_TO_EXTENSIONS_MAP.containsKey(consumer)) {
+ return;
+ }
+ final Consumer<Integer> extensionsConsumer = consumer::accept;
+ JAVA_TO_EXTENSIONS_MAP.put(consumer, extensionsConsumer);
+ addRearDisplayStatusListener(extensionsConsumer);
+ }
/**
* Removes a listener no longer interested in receiving updates.
* @param consumer no longer interested in receiving updates to WindowAreaStatus
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_2}
*/
- void removeRearDisplayStatusListener(@NonNull Consumer<@WindowAreaStatus Integer> consumer);
+ void removeRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
+
+ // TODO(b/264546746): Remove deprecated Window Extensions APIs after apps in g3 is updated to
+ // the latest library.
+ /**
+ * @deprecated Use {@link #removeRearDisplayStatusListener(Consumer)}.
+ *
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_2}
+ * @hide
+ */
+ @Deprecated
+ @SuppressLint("ClassVerificationFailure")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ default void removeRearDisplayStatusListener(
+ @NonNull java.util.function.Consumer<Integer> consumer) {
+ if (!JAVA_TO_EXTENSIONS_MAP.containsKey(consumer)) {
+ return;
+ }
+ final Consumer<Integer> extensionsConsumer = JAVA_TO_EXTENSIONS_MAP.remove(consumer);
+ removeRearDisplayStatusListener(extensionsConsumer);
+ }
/**
* Creates and starts a rear display session and sends state updates to the
* consumer provided. This consumer will receive a constant represented by
* [WindowAreaSessionState] to represent the state of the current rear display
- * session. We will translate the values from the {@link Consumer} to a developer-friendly
- * interface in the developer facing API.
+ * session. We will translate to a more friendly interface in the library.
*
* Because this is being called from the OEM provided extensions, the library
* will post the result of the listener on the executor provided by the developer.
@@ -135,14 +223,122 @@
* @throws UnsupportedOperationException if this method is called when RearDisplay
* mode is not available. This could be to an incompatible device state or when
* another process is currently in this mode.
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_2}
*/
+ @SuppressWarnings("ExecutorRegistration") // Jetpack will post it on the app-provided executor.
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.
+ /**
+ * @deprecated Use {@link #startRearDisplaySession(Activity, Consumer)}.
+ *
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_2}
+ * @hide
+ */
+ @Deprecated
+ @SuppressLint("ClassVerificationFailure")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ default void startRearDisplaySession(@NonNull Activity activity,
+ @NonNull java.util.function.Consumer<@WindowAreaSessionState Integer> consumer) {
+ final Consumer<Integer> extensionsConsumer = consumer::accept;
+ startRearDisplaySession(activity, extensionsConsumer);
+ }
+
/**
* Ends a RearDisplaySession and sends [STATE_INACTIVE] to the consumer
* provided in the {@code startRearDisplaySession} method. This method is only
* called through the {@code RearDisplaySession} provided to the developer.
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_2}
*/
void endRearDisplaySession();
+
+ /**
+ * Adds a listener interested in receiving updates on the rear display presentation status
+ * of the device. Because this is being called from the OEM provided
+ * extensions, the library will post the result of the listener on the executor
+ * provided by the developer.
+ *
+ * The listener provided will receive {@link ExtensionWindowAreaStatus} values that
+ * correspond to the current status of the feature.
+ *
+ * @param consumer interested in receiving updates to {@link ExtensionWindowAreaStatus}.
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_3}
+ */
+ default void addRearDisplayPresentationStatusListener(
+ @NonNull Consumer<ExtensionWindowAreaStatus> consumer) {
+ throw new UnsupportedOperationException("This method must not be called unless there is a"
+ + " corresponding override implementation on the device.");
+ }
+
+ /**
+ * Removes a listener no longer interested in receiving updates.
+ *
+ * @param consumer no longer interested in receiving updates to WindowAreaStatus
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_3}
+ */
+ default void removeRearDisplayPresentationStatusListener(
+ @NonNull Consumer<ExtensionWindowAreaStatus> consumer) {
+ throw new UnsupportedOperationException("This method must not be called unless there is a"
+ + " corresponding override implementation on the device.");
+ }
+
+ /**
+ * Creates and starts a rear display presentation session and sends state updates to the
+ * consumer provided. This consumer will receive a constant represented by
+ * {@link WindowAreaSessionState} to represent the state of the current rear display
+ * session. We will translate to a more friendly interface in the library.
+ *
+ * Because this is being called from the OEM provided extensions, the library
+ * will post the result of the listener on the executor provided by the developer.
+ *
+ * Rear display presentation mode refers to a feature where an {@link Activity} can present
+ * additional content on a device with a second display that is facing the same direction
+ * as the rear camera (i.e. the cover display on a fold-in style device). The calling
+ * {@link Activity} stays on the user-facing display.
+ *
+ * @param activity that the OEM implementation will use as a base
+ * context and to identify the source display area of the request.
+ * The reference to the activity instance must not be stored in the OEM
+ * implementation to prevent memory leaks.
+ * @param consumer to provide updates to the client on the status of the session
+ * @throws UnsupportedOperationException if this method is called when rear display presentation
+ * mode is not available. This could be to an incompatible device state or when
+ * another process is currently in this mode.
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_3}
+ */
+ default void startRearDisplayPresentationSession(@NonNull Activity activity,
+ @NonNull Consumer<@WindowAreaSessionState Integer> consumer) {
+ throw new UnsupportedOperationException("This method must not be called unless there is a"
+ + " corresponding override implementation on the device.");
+ }
+
+ /**
+ * Ends the current rear display presentation session and provides updates to the
+ * callback provided. When this is ended, the presented content from the calling
+ * {@link Activity} will also be removed from the rear facing display.
+ * Because this is being called from the OEM provided extensions, the result of the listener
+ * will be posted on the executor provided by the developer at the initial call site.
+ *
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_3}
+ */
+ default void endRearDisplayPresentationSession() {
+ throw new UnsupportedOperationException("This method must not be called unless there is a"
+ + " corresponding override implementation on the device.");
+ }
+
+ /**
+ * Returns the {@link ExtensionWindowAreaPresentation} connected to the active
+ * rear display presentation session. If there is no session currently active, then it will
+ * return null.
+ *
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_3}
+ */
+ @Nullable
+ default ExtensionWindowAreaPresentation getRearDisplayPresentation() {
+ 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/ActivityEmbeddingComponent.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
index bdca977..121520c 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
@@ -17,6 +17,8 @@
package androidx.window.extensions.embedding;
import android.app.Activity;
+import android.app.ActivityOptions;
+import android.os.IBinder;
import android.view.WindowMetrics;
import androidx.annotation.NonNull;
@@ -124,4 +126,60 @@
* Since {@link WindowExtensions#VENDOR_API_LEVEL_2}
*/
void clearSplitAttributesCalculator();
+
+ /**
+ * Sets the launching {@link ActivityStack} to the given {@link ActivityOptions}.
+ *
+ * @param options The {@link ActivityOptions} to be updated.
+ * @param token The {@link ActivityStack#getToken()} to represent the {@link ActivityStack}
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_3}
+ */
+ @NonNull
+ default ActivityOptions setLaunchingActivityStack(@NonNull ActivityOptions options,
+ @NonNull IBinder token) {
+ throw new UnsupportedOperationException("This method must not be called unless there is a"
+ + " corresponding override implementation on the device.");
+ }
+
+ /**
+ * Finishes a set of {@link ActivityStack}s. When an {@link ActivityStack} that was in an active
+ * split is finished, the other {@link ActivityStack} in the same {@link SplitInfo} can be
+ * expanded to fill the parent task container.
+ *
+ * @param activityStackTokens The set of tokens of {@link ActivityStack}-s that is going to be
+ * finished.
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_3}
+ */
+ default void finishActivityStacks(@NonNull Set<IBinder> activityStackTokens) {
+ throw new UnsupportedOperationException("This method must not be called unless there is a"
+ + " corresponding override implementation on the device.");
+ }
+
+ /**
+ * Triggers an update of the split attributes for the top split if there is one visible by
+ * making extensions invoke the split attributes calculator callback. This method can be used
+ * when a change to the split presentation originates from the application state change rather
+ * than driven by parent window changes or new activity starts. The call will be ignored if
+ * there is no visible split.
+ * @see #setSplitAttributesCalculator(Function)
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_3}
+ */
+ default void invalidateTopVisibleSplitAttributes() {
+ throw new UnsupportedOperationException("This method must not be called unless there is a"
+ + " corresponding override implementation on the device.");
+ }
+
+ /**
+ * Updates the {@link SplitAttributes} of a split pair. This is an alternative to using
+ * a split attributes calculator callback, applicable when apps only need to update the
+ * splits in a few cases but rely on the default split attributes otherwise.
+ * @param splitInfoToken The identifier of the split pair to update.
+ * @param splitAttributes The {@link SplitAttributes} to apply to the split pair.
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_3}
+ */
+ default void updateSplitAttributes(@NonNull IBinder splitInfoToken,
+ @NonNull SplitAttributes splitAttributes) {
+ 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/ActivityStack.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
index 857738d..e568666 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
@@ -17,8 +17,12 @@
package androidx.window.extensions.embedding;
import android.app.Activity;
+import android.os.Binder;
+import android.os.IBinder;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.WindowExtensions;
import java.util.ArrayList;
import java.util.List;
@@ -30,11 +34,17 @@
*/
public class ActivityStack {
+ /** Only used for compatibility with the deprecated constructor. */
+ private static final IBinder INVALID_ACTIVITY_STACK_TOKEN = new Binder();
+
@NonNull
private final List<Activity> mActivities;
private final boolean mIsEmpty;
+ @NonNull
+ private final IBinder mToken;
+
/**
* The {@code ActivityStack} constructor
*
@@ -42,11 +52,24 @@
* belongs to this {@code ActivityStack}
* @param isEmpty Indicates whether there's any {@link Activity} running in this
* {@code ActivityStack}
+ * @param token The token to identify this {@code ActivityStack}
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_3}
*/
- ActivityStack(@NonNull List<Activity> activities, boolean isEmpty) {
+ ActivityStack(@NonNull List<Activity> activities, boolean isEmpty, @NonNull IBinder token) {
Objects.requireNonNull(activities);
+ Objects.requireNonNull(token);
mActivities = new ArrayList<>(activities);
mIsEmpty = isEmpty;
+ mToken = token;
+ }
+
+ /**
+ * @deprecated Use the {@link WindowExtensions#VENDOR_API_LEVEL_3} version.
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_1}
+ */
+ @Deprecated
+ ActivityStack(@NonNull List<Activity> activities, boolean isEmpty) {
+ this(activities, isEmpty, INVALID_ACTIVITY_STACK_TOKEN);
}
/**
@@ -76,19 +99,31 @@
return mIsEmpty;
}
+ /**
+ * Returns a token uniquely identifying the container.
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_3}
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public IBinder getToken() {
+ return mToken;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ActivityStack)) return false;
ActivityStack that = (ActivityStack) o;
return mActivities.equals(that.mActivities)
- && mIsEmpty == that.mIsEmpty;
+ && mIsEmpty == that.mIsEmpty
+ && mToken.equals(that.mToken);
}
@Override
public int hashCode() {
int result = (mIsEmpty ? 1 : 0);
result = result * 31 + mActivities.hashCode();
+ result = result * 31 + mToken.hashCode();
return result;
}
@@ -97,6 +132,7 @@
public String toString() {
return "ActivityStack{" + "mActivities=" + mActivities
+ ", mIsEmpty=" + mIsEmpty
+ + ", mToken=" + mToken
+ '}';
}
}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculator.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculator.java
new file mode 100644
index 0000000..b06fefa
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculator.java
@@ -0,0 +1,166 @@
+/*
+ * 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.window.extensions.embedding;
+
+import android.content.res.Configuration;
+import android.os.Build;
+import android.view.WindowMetrics;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.layout.WindowLayoutInfo;
+
+// TODO(b/264546746): Remove deprecated Window Extensions APIs after apps in g3 is updated to the
+// latest library.
+/**
+ * @deprecated Use {@link androidx.window.extensions.core.util.function.Function} instead unless
+ * {@link androidx.window.extensions.core.util.function.Function} cannot be used.
+ *
+ * @hide
+ */
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface SplitAttributesCalculator {
+ /** @deprecated See {@link SplitAttributesCalculator}. */
+ @Deprecated
+ @NonNull
+ SplitAttributes computeSplitAttributesForParams(
+ @NonNull SplitAttributesCalculatorParams params);
+
+ /**
+ * @deprecated Use
+ * {@link androidx.window.extensions.embedding.SplitAttributesCalculatorParams} unless
+ * {@link androidx.window.extensions.embedding.SplitAttributesCalculatorParams} cannot be used.
+ */
+ @Deprecated
+ class SplitAttributesCalculatorParams {
+ @NonNull
+ private final WindowMetrics mParentWindowMetrics;
+ @NonNull
+ private final Configuration mParentConfiguration;
+ @NonNull
+ private final WindowLayoutInfo mParentWindowLayoutInfo;
+ @NonNull
+ private final SplitAttributes mDefaultSplitAttributes;
+ private final boolean mIsDefaultMinSizeSatisfied;
+ @Nullable
+ private final String mSplitRuleTag;
+
+ /** Returns the parent container's {@link WindowMetrics} */
+ @NonNull
+ public WindowMetrics getParentWindowMetrics() {
+ return mParentWindowMetrics;
+ }
+
+ /** Returns the parent container's {@link Configuration} */
+ @NonNull
+ public Configuration getParentConfiguration() {
+ return new Configuration(mParentConfiguration);
+ }
+
+ /**
+ * Returns the {@link SplitRule#getDefaultSplitAttributes()}. It could be from
+ * {@link SplitRule} Builder APIs
+ * ({@link SplitPairRule.Builder#setDefaultSplitAttributes(SplitAttributes)} or
+ * {@link SplitPlaceholderRule.Builder#setDefaultSplitAttributes(SplitAttributes)}) or from
+ * the {@code splitRatio} and {@code splitLayoutDirection} attributes from static rule
+ * definitions.
+ */
+ @NonNull
+ public SplitAttributes getDefaultSplitAttributes() {
+ return mDefaultSplitAttributes;
+ }
+
+ /**
+ * Returns whether the {@link #getParentWindowMetrics()} satisfies the dimensions and aspect
+ * ratios requirements specified in the {@link androidx.window.embedding.SplitRule}, which
+ * are:
+ * - {@link androidx.window.embedding.SplitRule#minWidthDp}
+ * - {@link androidx.window.embedding.SplitRule#minHeightDp}
+ * - {@link androidx.window.embedding.SplitRule#minSmallestWidthDp}
+ * - {@link androidx.window.embedding.SplitRule#maxAspectRatioInPortrait}
+ * - {@link androidx.window.embedding.SplitRule#maxAspectRatioInLandscape}
+ */
+ public boolean isDefaultMinSizeSatisfied() {
+ return mIsDefaultMinSizeSatisfied;
+ }
+
+ /** Returns the parent container's {@link WindowLayoutInfo} */
+ @NonNull
+ public WindowLayoutInfo getParentWindowLayoutInfo() {
+ return mParentWindowLayoutInfo;
+ }
+
+ /**
+ * Returns {@link SplitRule#getTag()} to apply the {@link SplitAttributes} result if it was
+ * set.
+ */
+ @Nullable
+ public String getSplitRuleTag() {
+ return mSplitRuleTag;
+ }
+
+ SplitAttributesCalculatorParams(
+ @NonNull WindowMetrics parentWindowMetrics,
+ @NonNull Configuration parentConfiguration,
+ @NonNull SplitAttributes defaultSplitAttributes,
+ boolean isDefaultMinSizeSatisfied,
+ @NonNull WindowLayoutInfo parentWindowLayoutInfo,
+ @Nullable String splitRuleTag
+ ) {
+ mParentWindowMetrics = parentWindowMetrics;
+ mParentConfiguration = parentConfiguration;
+ mParentWindowLayoutInfo = parentWindowLayoutInfo;
+ mDefaultSplitAttributes = defaultSplitAttributes;
+ mIsDefaultMinSizeSatisfied = isDefaultMinSizeSatisfied;
+ mSplitRuleTag = splitRuleTag;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ":{"
+ + "windowMetrics=" + windowMetricsToString(mParentWindowMetrics)
+ + ", configuration=" + mParentConfiguration
+ + ", windowLayoutInfo=" + mParentWindowLayoutInfo
+ + ", defaultSplitAttributes=" + mDefaultSplitAttributes
+ + ", isDefaultMinSizeSatisfied=" + mIsDefaultMinSizeSatisfied
+ + ", tag=" + mSplitRuleTag + "}";
+ }
+
+ private static String windowMetricsToString(@NonNull WindowMetrics windowMetrics) {
+ // TODO(b/187712731): Use WindowMetrics#toString after it's implemented in U.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ return Api30Impl.windowMetricsToString(
+ windowMetrics);
+ }
+ throw new UnsupportedOperationException("WindowMetrics didn't exist in R.");
+ }
+
+ @RequiresApi(30)
+ private static final class Api30Impl {
+ static String windowMetricsToString(@NonNull WindowMetrics windowMetrics) {
+ return WindowMetrics.class.getSimpleName() + ":{"
+ + "bounds=" + windowMetrics.getBounds()
+ + ", windowInsets=" + windowMetrics.getWindowInsets()
+ + "}";
+ }
+ }
+ }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java
index 33b8bb7..bac42a4 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java
@@ -16,6 +16,9 @@
package androidx.window.extensions.embedding;
+import android.os.Binder;
+import android.os.IBinder;
+
import androidx.annotation.NonNull;
import androidx.window.extensions.WindowExtensions;
import androidx.window.extensions.embedding.SplitAttributes.SplitType;
@@ -25,6 +28,9 @@
/** Describes a split of two containers with activities. */
public class SplitInfo {
+ /** Only used for compatibility with the deprecated constructor. */
+ private static final IBinder INVALID_SPLIT_INFO_TOKEN = new Binder();
+
@NonNull
private final ActivityStack mPrimaryActivityStack;
@NonNull
@@ -32,22 +38,42 @@
@NonNull
private final SplitAttributes mSplitAttributes;
+ @NonNull
+ private final IBinder mToken;
+
/**
- * The {@code SplitInfo} constructor.
+ * The {@code SplitInfo} constructor
*
- * @param primaryActivityStack The primary {@link ActivityStack}.
- * @param secondaryActivityStack The secondary {@link ActivityStack}.
- * @param splitAttributes The current {@link SplitAttributes} of this split pair.
+ * @param primaryActivityStack The primary {@link ActivityStack}
+ * @param secondaryActivityStack The secondary {@link ActivityStack}
+ * @param splitAttributes The current {@link SplitAttributes} of this split pair
+ * @param token The token to identify this split pair
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_3}
*/
SplitInfo(@NonNull ActivityStack primaryActivityStack,
@NonNull ActivityStack secondaryActivityStack,
- @NonNull SplitAttributes splitAttributes) {
+ @NonNull SplitAttributes splitAttributes,
+ @NonNull IBinder token) {
Objects.requireNonNull(primaryActivityStack);
Objects.requireNonNull(secondaryActivityStack);
Objects.requireNonNull(splitAttributes);
+ Objects.requireNonNull(token);
mPrimaryActivityStack = primaryActivityStack;
mSecondaryActivityStack = secondaryActivityStack;
mSplitAttributes = splitAttributes;
+ mToken = token;
+ }
+
+ /**
+ * @deprecated Use the {@link WindowExtensions#VENDOR_API_LEVEL_3} version.
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_1}
+ */
+ @Deprecated
+ SplitInfo(@NonNull ActivityStack primaryActivityStack,
+ @NonNull ActivityStack secondaryActivityStack,
+ @NonNull SplitAttributes splitAttributes) {
+ this(primaryActivityStack, secondaryActivityStack, splitAttributes,
+ INVALID_SPLIT_INFO_TOKEN);
}
@NonNull
@@ -84,6 +110,15 @@
return mSplitAttributes;
}
+ /**
+ * Returns a token uniquely identifying the container.
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_3}
+ */
+ @NonNull
+ public IBinder getToken() {
+ return mToken;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -91,7 +126,7 @@
SplitInfo that = (SplitInfo) o;
return mSplitAttributes.equals(that.mSplitAttributes) && mPrimaryActivityStack.equals(
that.mPrimaryActivityStack) && mSecondaryActivityStack.equals(
- that.mSecondaryActivityStack);
+ that.mSecondaryActivityStack) && mToken.equals(that.mToken);
}
@Override
@@ -99,6 +134,7 @@
int result = mPrimaryActivityStack.hashCode();
result = result * 31 + mSecondaryActivityStack.hashCode();
result = result * 31 + mSplitAttributes.hashCode();
+ result = result * 31 + mToken.hashCode();
return result;
}
@@ -109,6 +145,7 @@
+ "mPrimaryActivityStack=" + mPrimaryActivityStack
+ ", mSecondaryActivityStack=" + mSecondaryActivityStack
+ ", mSplitAttributes=" + mSplitAttributes
+ + ", mToken=" + mToken
+ '}';
}
}
diff --git a/window/extensions/extensions/src/test/resources/robolectric.properties b/window/extensions/extensions/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..69fde47
--- /dev/null
+++ b/window/extensions/extensions/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/window/sidecar/sidecar/build.gradle b/window/sidecar/sidecar/build.gradle
index 742646c..67a5ab4 100644
--- a/window/sidecar/sidecar/build.gradle
+++ b/window/sidecar/sidecar/build.gradle
@@ -27,7 +27,7 @@
}
androidx {
- name = "Jetpack WindowManager library Sidecar"
+ name = "WindowManager Sidecar"
publish = Publish.SNAPSHOT_AND_RELEASE // Only to generate per-project-zips
runApiTasks = new RunApiTasks.Yes("Need to track API surface but should never publish")
inceptionYear = "2020"
diff --git a/window/window-core/build.gradle b/window/window-core/build.gradle
index e0e80a3..fbaefda 100644
--- a/window/window-core/build.gradle
+++ b/window/window-core/build.gradle
@@ -36,7 +36,7 @@
}
androidx {
- name = "androidx.window:window-core"
+ name = "WindowManager Core"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2022"
description = "WindowManager Core Library."
diff --git a/window/window-demos/demo/build.gradle b/window/window-demos/demo/build.gradle
index 65adb00..cd3bcf4 100644
--- a/window/window-demos/demo/build.gradle
+++ b/window/window-demos/demo/build.gradle
@@ -82,7 +82,7 @@
}
androidx {
- name = "WM Jetpack Samples"
+ name = "WM Samples"
type = LibraryType.SAMPLES
inceptionYear = "2023"
description = "Samples for the WM Jetpack Library"
diff --git a/window/window-demos/demo/src/main/AndroidManifest.xml b/window/window-demos/demo/src/main/AndroidManifest.xml
index 5d3151e..48d5ebf 100644
--- a/window/window-demos/demo/src/main/AndroidManifest.xml
+++ b/window/window-demos/demo/src/main/AndroidManifest.xml
@@ -58,7 +58,7 @@
android:exported="false"
android:configChanges="orientation|screenSize|screenLayout|screenSize"
android:label="@string/window_metrics"/>
- <activity android:name=".area.RearDisplayActivityConfigChanges"
+ <activity android:name=".RearDisplayActivityConfigChanges"
android:exported="true"
android:configChanges=
"orientation|screenLayout|screenSize|layoutDirection|smallestScreenSize"
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/RearDisplayActivityConfigChanges.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/RearDisplayActivityConfigChanges.kt
new file mode 100644
index 0000000..3d35924
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/RearDisplayActivityConfigChanges.kt
@@ -0,0 +1,168 @@
+/*
+ * 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
+
+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.demo.common.infolog.InfoLogAdapter
+import androidx.window.demo.databinding.ActivityRearDisplayBinding
+import java.text.SimpleDateFormat
+import java.util.Date
+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
+
+/**
+ * Demo Activity that showcases listening for RearDisplay Status
+ * as well as enabling/disabling RearDisplay mode. This Activity
+ * implements [WindowAreaSessionCallback] for simplicity.
+ *
+ * This Activity overrides configuration changes for simplicity.
+ */
+class RearDisplayActivityConfigChanges : AppCompatActivity(), 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
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityRearDisplayBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ executor = ContextCompat.getMainExecutor(this)
+ windowAreaController = WindowAreaController.getOrCreate()
+
+ binding.rearStatusRecyclerView.adapter = infoLogAdapter
+
+ binding.rearDisplayButton.setOnClickListener {
+ if (rearDisplayStatus == WINDOW_AREA_STATUS_ACTIVE) {
+ if (rearDisplaySession == null) {
+ rearDisplaySession = rearDisplayWindowAreaInfo?.getActiveSession(
+ OPERATION_TRANSFER_ACTIVITY_TO_AREA
+ )
+ }
+ rearDisplaySession?.close()
+ } else {
+ rearDisplayWindowAreaInfo?.token?.let { token ->
+ windowAreaController.transferActivityToWindowArea(
+ token = token,
+ activity = this,
+ executor = executor,
+ windowAreaSessionCallback = 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()
+ }
+ }
+ }
+ }
+
+ override fun onSessionStarted(session: WindowAreaSession) {
+ rearDisplaySession = session
+ infoLogAdapter.append(getCurrentTimeString(), "RearDisplay Session has been started")
+ infoLogAdapter.notifyDataSetChanged()
+ updateRearDisplayButton()
+ }
+
+ override fun onSessionEnded(t: Throwable?) {
+ rearDisplaySession = null
+ infoLogAdapter.append(getCurrentTimeString(), "RearDisplay Session has ended")
+ infoLogAdapter.notifyDataSetChanged()
+ updateRearDisplayButton()
+ }
+
+ private fun updateRearDisplayButton() {
+ if (rearDisplaySession != null) {
+ binding.rearDisplayButton.isEnabled = true
+ 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"
+ }
+ }
+ }
+
+ private fun getCurrentTimeString(): String {
+ val sdf = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
+ val currentDate = sdf.format(Date())
+ 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
+ }
+}
\ No newline at end of file
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
deleted file mode 100644
index edb2ed1..0000000
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayActivityConfigChanges.kt
+++ /dev/null
@@ -1,136 +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.area
-
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.content.ContextCompat
-import androidx.core.util.Consumer
-import androidx.window.area.WindowAreaController
-import androidx.window.area.WindowAreaSession
-import androidx.window.area.WindowAreaSessionCallback
-import androidx.window.area.WindowAreaStatus
-import androidx.window.core.ExperimentalWindowApi
-import androidx.window.demo.common.infolog.InfoLogAdapter
-import androidx.window.demo.databinding.ActivityRearDisplayBinding
-import androidx.window.java.area.WindowAreaControllerJavaAdapter
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
-import java.util.concurrent.Executor
-
-/**
- * Demo Activity that showcases listening for RearDisplay Status
- * as well as enabling/disabling RearDisplay mode. This Activity
- * implements [WindowAreaSessionCallback] for simplicity.
- *
- * This Activity overrides configuration changes for simplicity.
- */
-@OptIn(ExperimentalWindowApi::class)
-class RearDisplayActivityConfigChanges : AppCompatActivity(), WindowAreaSessionCallback {
-
- private lateinit var windowAreaController: WindowAreaControllerJavaAdapter
- private var rearDisplaySession: WindowAreaSession? = null
- private val infoLogAdapter = InfoLogAdapter()
- private lateinit var binding: ActivityRearDisplayBinding
- private lateinit var executor: Executor
-
- private val rearDisplayStatusListener = Consumer<WindowAreaStatus> { status ->
- infoLogAdapter.append(getCurrentTimeString(), status.toString())
- infoLogAdapter.notifyDataSetChanged()
- updateRearDisplayButton(status)
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityRearDisplayBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- executor = ContextCompat.getMainExecutor(this)
- windowAreaController = WindowAreaControllerJavaAdapter(WindowAreaController.getOrCreate())
-
- binding.rearStatusRecyclerView.adapter = infoLogAdapter
-
- binding.rearDisplayButton.setOnClickListener {
- if (rearDisplaySession != null) {
- rearDisplaySession?.close()
- } else {
- windowAreaController.startRearDisplayModeSession(
- this,
- executor,
- this)
- }
- }
- }
-
- override fun onStart() {
- super.onStart()
- windowAreaController.addRearDisplayStatusListener(
- executor,
- rearDisplayStatusListener
- )
- }
-
- override fun onStop() {
- super.onStop()
- windowAreaController.removeRearDisplayStatusListener(rearDisplayStatusListener)
- }
-
- override fun onSessionStarted(session: WindowAreaSession) {
- rearDisplaySession = session
- infoLogAdapter.append(getCurrentTimeString(), "RearDisplay Session has been started")
- infoLogAdapter.notifyDataSetChanged()
- }
-
- override fun onSessionEnded() {
- rearDisplaySession = null
- infoLogAdapter.append(getCurrentTimeString(), "RearDisplay Session has ended")
- infoLogAdapter.notifyDataSetChanged()
- }
-
- private fun updateRearDisplayButton(status: WindowAreaStatus) {
- if (rearDisplaySession != null) {
- binding.rearDisplayButton.isEnabled = true
- binding.rearDisplayButton.text = "Disable RearDisplay Mode"
- return
- }
- when (status) {
- WindowAreaStatus.UNSUPPORTED -> {
- binding.rearDisplayButton.isEnabled = false
- binding.rearDisplayButton.text = "RearDisplay is not supported on this device"
- }
- WindowAreaStatus.UNAVAILABLE -> {
- binding.rearDisplayButton.isEnabled = false
- binding.rearDisplayButton.text = "RearDisplay is not currently available"
- }
- WindowAreaStatus.AVAILABLE -> {
- binding.rearDisplayButton.isEnabled = true
- binding.rearDisplayButton.text = "Enable RearDisplay Mode"
- }
- }
- }
-
- private fun getCurrentTimeString(): String {
- val sdf = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
- val currentDate = sdf.format(Date())
- return currentDate.toString()
- }
-
- private companion object {
- private val TAG = RearDisplayActivityConfigChanges::class.java.simpleName
- }
-}
\ No newline at end of file
diff --git a/window/window-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 7cfc7ee..c2ffc78 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
@@ -18,7 +18,6 @@
import android.content.Context
import androidx.startup.Initializer
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.demo.R
import androidx.window.demo.embedding.SplitDeviceStateActivityBase.Companion.SUFFIX_AND_FULLSCREEN_IN_BOOK_MODE
import androidx.window.demo.embedding.SplitDeviceStateActivityBase.Companion.SUFFIX_AND_HORIZONTAL_LAYOUT_IN_TABLETOP
@@ -46,7 +45,6 @@
/**
* Initializes SplitController with a set of statically defined rules.
*/
-@OptIn(ExperimentalWindowApi::class)
class ExampleWindowInitializer : Initializer<RuleController> {
override fun create(context: Context): RuleController {
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 8e0c564..faf792e 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
@@ -24,6 +24,7 @@
import static androidx.window.embedding.SplitRule.FinishBehavior.NEVER;
import android.app.Activity;
+import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
@@ -41,6 +42,7 @@
import androidx.window.demo.R;
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.EmbeddingRule;
@@ -96,8 +98,23 @@
bStartIntent.putExtra(EXTRA_LAUNCH_C_TO_SIDE, true);
startActivity(bStartIntent);
});
- mViewBinding.launchE.setOnClickListener((View v) ->
- startActivity(new Intent(this, SplitActivityE.class)));
+ mViewBinding.launchE.setOnClickListener((View v) -> {
+ Bundle bundle = null;
+ if (mViewBinding.setLaunchingEInActivityStack.isChecked()) {
+ try {
+ final ActivityOptions options = ActivityEmbeddingOptions
+ .setLaunchingActivityStack(ActivityOptions.makeBasic(), this);
+ bundle = options.toBundle();
+ } catch (UnsupportedOperationException ex) {
+ Log.w(TAG, "#setLaunchingActivityStack is not supported", ex);
+ }
+ }
+ startActivity(new Intent(this, SplitActivityE.class), bundle);
+ });
+ if (!ActivityEmbeddingOptions.isSetLaunchingActivityStackSupported(
+ ActivityOptions.makeBasic())) {
+ mViewBinding.setLaunchingEInActivityStack.setEnabled(false);
+ }
mViewBinding.launchF.setOnClickListener((View v) ->
startActivity(new Intent(this, SplitActivityF.class)));
mViewBinding.launchFPendingIntent.setOnClickListener((View v) -> {
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 9f80a5f..e95997c 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
@@ -28,7 +28,6 @@
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.demo.R
import androidx.window.demo.databinding.ActivitySplitDeviceStateLayoutBinding
import androidx.window.embedding.EmbeddingRule
@@ -45,7 +44,6 @@
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-@OptIn(ExperimentalWindowApi::class)
open class SplitDeviceStateActivityBase : AppCompatActivity(), View.OnClickListener,
RadioGroup.OnCheckedChangeListener, CompoundButton.OnCheckedChangeListener,
AdapterView.OnItemSelectedListener {
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 099490a..dcc8ab4 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
@@ -161,4 +161,4 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
-</ScrollView>
+</ScrollView>
\ No newline at end of file
diff --git a/window/window-java/api/current.txt b/window/window-java/api/current.txt
index 39c35ac..aa2af8f 100644
--- a/window/window-java/api/current.txt
+++ b/window/window-java/api/current.txt
@@ -1,4 +1,14 @@
// Signature format: 4.0
+package androidx.window.java.area {
+
+ public final class WindowAreaControllerCallbackAdapter implements androidx.window.area.WindowAreaController {
+ ctor public WindowAreaControllerCallbackAdapter(androidx.window.area.WindowAreaController controller);
+ method public void addWindowAreaInfoListListener(java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.area.WindowAreaInfo>> listener);
+ method public void removeWindowAreaInfoListListener(androidx.core.util.Consumer<java.util.List<androidx.window.area.WindowAreaInfo>> listener);
+ }
+
+}
+
package androidx.window.java.layout {
public final class WindowInfoTrackerCallbackAdapter implements androidx.window.layout.WindowInfoTracker {
diff --git a/window/window-java/api/public_plus_experimental_current.txt b/window/window-java/api/public_plus_experimental_current.txt
index d621966..d443b31 100644
--- a/window/window-java/api/public_plus_experimental_current.txt
+++ b/window/window-java/api/public_plus_experimental_current.txt
@@ -1,4 +1,14 @@
// Signature format: 4.0
+package androidx.window.java.area {
+
+ public final class WindowAreaControllerCallbackAdapter implements androidx.window.area.WindowAreaController {
+ ctor public WindowAreaControllerCallbackAdapter(androidx.window.area.WindowAreaController controller);
+ method public void addWindowAreaInfoListListener(java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.area.WindowAreaInfo>> listener);
+ method public void removeWindowAreaInfoListListener(androidx.core.util.Consumer<java.util.List<androidx.window.area.WindowAreaInfo>> listener);
+ }
+
+}
+
package androidx.window.java.embedding {
@androidx.window.core.ExperimentalWindowApi public final class SplitControllerCallbackAdapter {
diff --git a/window/window-java/api/restricted_current.txt b/window/window-java/api/restricted_current.txt
index 39c35ac..aa2af8f 100644
--- a/window/window-java/api/restricted_current.txt
+++ b/window/window-java/api/restricted_current.txt
@@ -1,4 +1,14 @@
// Signature format: 4.0
+package androidx.window.java.area {
+
+ public final class WindowAreaControllerCallbackAdapter implements androidx.window.area.WindowAreaController {
+ ctor public WindowAreaControllerCallbackAdapter(androidx.window.area.WindowAreaController controller);
+ method public void addWindowAreaInfoListListener(java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.area.WindowAreaInfo>> listener);
+ method public void removeWindowAreaInfoListListener(androidx.core.util.Consumer<java.util.List<androidx.window.area.WindowAreaInfo>> listener);
+ }
+
+}
+
package androidx.window.java.layout {
public final class WindowInfoTrackerCallbackAdapter implements androidx.window.layout.WindowInfoTracker {
diff --git a/window/window-java/build.gradle b/window/window-java/build.gradle
index ad4a7de..36a1f22 100644
--- a/window/window-java/build.gradle
+++ b/window/window-java/build.gradle
@@ -46,7 +46,7 @@
}
androidx {
- name = "WindowManager Java Support"
+ name = "WindowManager Java"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2021"
description = "WindowManager Java Support"
diff --git a/window/window-java/src/main/java/androidx/window/java/area/WindowAreaControllerCallbackAdapter.kt b/window/window-java/src/main/java/androidx/window/java/area/WindowAreaControllerCallbackAdapter.kt
new file mode 100644
index 0000000..09c8292
--- /dev/null
+++ b/window/window-java/src/main/java/androidx/window/java/area/WindowAreaControllerCallbackAdapter.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.java.area
+
+import androidx.core.util.Consumer
+import androidx.window.area.WindowAreaController
+import androidx.window.area.WindowAreaInfo
+import java.util.concurrent.Executor
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.launch
+
+/**
+ * An adapter for [WindowAreaController] to provide callback APIs.
+ */
+class WindowAreaControllerCallbackAdapter(
+ private val controller: WindowAreaController
+) : WindowAreaController by controller {
+
+ /**
+ * A [ReentrantLock] to protect against concurrent access to [consumerToJobMap].
+ */
+ private val lock = ReentrantLock()
+ private val consumerToJobMap = mutableMapOf<Consumer<*>, Job>()
+
+ /**
+ * Registers a listener that is interested in the current list of [WindowAreaInfo] available to
+ * be interacted with.
+ *
+ * The [listener] will receive an initial value on registration, as soon as it becomes
+ * available.
+ *
+ * @param executor to handle sending listener updates.
+ * @param listener to receive updates to the list of [WindowAreaInfo].
+ * @see WindowAreaController.transferActivityToWindowArea
+ * @see WindowAreaController.presentContentOnWindowArea
+ */
+ fun addWindowAreaInfoListListener(
+ executor: Executor,
+ listener: Consumer<List<WindowAreaInfo>>
+ ) {
+ // TODO(274013517): Extract adapter pattern out of each class
+ val statusFlow = controller.windowAreaInfos
+ lock.withLock {
+ if (consumerToJobMap[listener] == null) {
+ val scope = CoroutineScope(executor.asCoroutineDispatcher())
+ consumerToJobMap[listener] = scope.launch {
+ statusFlow.collect { listener.accept(it) }
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes a listener of available [WindowAreaInfo] records. If the listener is not present then
+ * this method is a no-op.
+ *
+ * @param listener to remove from receiving status updates.
+ * @see WindowAreaController.transferActivityToWindowArea
+ * @see WindowAreaController.presentContentOnWindowArea
+ */
+ fun removeWindowAreaInfoListListener(listener: Consumer<List<WindowAreaInfo>>) {
+ lock.withLock {
+ consumerToJobMap[listener]?.cancel()
+ consumerToJobMap.remove(listener)
+ }
+ }
+ }
\ No newline at end of file
diff --git a/window/window-java/src/main/java/androidx/window/java/area/WindowAreaControllerJavaAdapter.kt b/window/window-java/src/main/java/androidx/window/java/area/WindowAreaControllerJavaAdapter.kt
deleted file mode 100644
index c2f21fe..0000000
--- a/window/window-java/src/main/java/androidx/window/java/area/WindowAreaControllerJavaAdapter.kt
+++ /dev/null
@@ -1,128 +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.java.area
-
-import android.app.Activity
-import androidx.core.util.Consumer
-import androidx.window.area.WindowAreaSessionCallback
-import androidx.window.area.WindowAreaStatus
-import androidx.window.area.WindowAreaController
-import androidx.window.core.ExperimentalWindowApi
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.asCoroutineDispatcher
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.launch
-import java.util.concurrent.Executor
-import java.util.concurrent.locks.ReentrantLock
-import kotlin.concurrent.withLock
-
-/**
- * An adapted interface for [WindowAreaController] that provides the information and
- * functionality around RearDisplay Mode via a callback shaped API.
- *
- * @hide
- *
- */
-@ExperimentalWindowApi
-class WindowAreaControllerJavaAdapter(
- private val controller: WindowAreaController
-) : WindowAreaController by controller {
-
- /**
- * A [ReentrantLock] to protect against concurrent access to [consumerToJobMap].
- */
- private val lock = ReentrantLock()
- private val consumerToJobMap = mutableMapOf<Consumer<*>, Job>()
-
- /**
- * Registers a listener to consume [WindowAreaStatus] values defined as
- * [WindowAreaStatus.UNSUPPORTED], [WindowAreaStatus.UNAVAILABLE], and
- * [WindowAreaStatus.AVAILABLE]. The values provided through this listener should be used
- * to determine if you are able to enable rear display Mode at that time. You can use these
- * values to modify your UI to show/hide controls and determine when to enable features
- * that use rear display Mode. You should only try and enter rear display mode when your
- * [consumer] is provided a value of [WindowAreaStatus.AVAILABLE].
- *
- * The [consumer] will be provided an initial value on registration, as well as any change
- * to the status as they occur. This could happen due to hardware device state changes, or if
- * another process has enabled RearDisplay Mode.
- *
- * @see WindowAreaController.rearDisplayStatus
- */
- fun addRearDisplayStatusListener(
- executor: Executor,
- consumer: Consumer<WindowAreaStatus>
- ) {
- val statusFlow = controller.rearDisplayStatus()
- lock.withLock {
- if (consumerToJobMap[consumer] == null) {
- val scope = CoroutineScope(executor.asCoroutineDispatcher())
- consumerToJobMap[consumer] = scope.launch {
- statusFlow.collect { consumer.accept(it) }
- }
- }
- }
- }
-
- /**
- * Removes a listener of [WindowAreaStatus] values
- * @see WindowAreaController.rearDisplayStatus
- */
- fun removeRearDisplayStatusListener(consumer: Consumer<WindowAreaStatus>) {
- lock.withLock {
- consumerToJobMap[consumer]?.cancel()
- consumerToJobMap.remove(consumer)
- }
- }
-
- /**
- * Starts a RearDisplay Mode session and provides updates through the
- * [WindowAreaSessionCallback] provided. Due to the nature of moving your Activity to a
- * different display, your Activity will likely go through a configuration change. Because of
- * this, if your Activity does not override configuration changes, this method should be called
- * from a component that outlives the Activity lifecycle such as a
- * [androidx.lifecycle.ViewModel]. If your Activity does override
- * configuration changes, it is safe to call this method inside your Activity.
- *
- * This method should only be called if you have received a [WindowAreaStatus.AVAILABLE]
- * value from the listener provided through the [addRearDisplayStatusListener] method. If
- * you try and enable RearDisplay mode without it being available, you will receive an
- * [UnsupportedOperationException].
- *
- * The [windowAreaSessionCallback] provided will receive a call to
- * [WindowAreaSessionCallback.onSessionStarted] after your Activity has been moved to the
- * display corresponding to this mode. RearDisplay mode will stay active until the session
- * provided through [WindowAreaSessionCallback.onSessionStarted] is closed, or there is a device
- * state change that makes RearDisplay mode incompatible such as if the device is closed so the
- * outer-display is no longer in line with the rear camera. When this occurs,
- * [WindowAreaSessionCallback.onSessionEnded] is called to notify you the session has been
- * ended.
- *
- * @see addRearDisplayStatusListener
- * @throws UnsupportedOperationException if you try and start a RearDisplay session when
- * your [WindowAreaController.rearDisplayStatus] does not return a value of
- * [WindowAreaStatus.AVAILABLE]
- */
- fun startRearDisplayModeSession(
- activity: Activity,
- executor: Executor,
- windowAreaSessionCallback: WindowAreaSessionCallback
- ) {
- controller.rearDisplayMode(activity, executor, windowAreaSessionCallback)
- }
-}
\ No newline at end of file
diff --git a/window/window-rxjava2/build.gradle b/window/window-rxjava2/build.gradle
index 97a6430..f3d1277 100644
--- a/window/window-rxjava2/build.gradle
+++ b/window/window-rxjava2/build.gradle
@@ -50,7 +50,7 @@
}
androidx {
- name = "WindowManager RxJava 2"
+ name = "WindowManager RxJava2"
publish = Publish.SNAPSHOT_AND_RELEASE
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2021"
diff --git a/window/window-rxjava3/build.gradle b/window/window-rxjava3/build.gradle
index 2ab8d1a..e4962e8 100644
--- a/window/window-rxjava3/build.gradle
+++ b/window/window-rxjava3/build.gradle
@@ -49,7 +49,7 @@
}
androidx {
- name = "WindowManager RxJava 3 Support"
+ name = "WindowManager RxJava3"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2021"
description = "WindowManager RxJava 3 Support"
diff --git a/window/window-testing/build.gradle b/window/window-testing/build.gradle
index 9ccb288..7996b4b 100644
--- a/window/window-testing/build.gradle
+++ b/window/window-testing/build.gradle
@@ -55,7 +55,7 @@
}
androidx {
- name = "WindowManager Test Library"
+ name = "WindowManager Testing Extensions"
type = LibraryType.PUBLISHED_TEST_LIBRARY
inceptionYear = "2021"
description = "WindowManager Test Library"
diff --git a/window/window-testing/lint-baseline.xml b/window/window-testing/lint-baseline.xml
new file mode 100644
index 0000000..4225368
--- /dev/null
+++ b/window/window-testing/lint-baseline.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.1.0-alpha07" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-beta03)" variant="all" version="8.1.0-alpha07">
+
+ <issue
+ id="BanHideAnnotation"
+ message="@hide is not allowed in Javadoc"
+ errorLine1="val TEST_ACTIVITY_STACK_TOKEN = Binder()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt"/>
+ </issue>
+
+</issues>
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 ef38d1a..396936b 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,6 +18,9 @@
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
/**
@@ -37,4 +40,10 @@
fun TestActivityStack(
activitiesInProcess: List<Activity> = emptyList(),
isEmpty: Boolean = false,
-): ActivityStack = ActivityStack(activitiesInProcess, isEmpty)
\ No newline at end of file
+): ActivityStack = ActivityStack(activitiesInProcess, isEmpty, TEST_ACTIVITY_STACK_TOKEN)
+
+/** @hide */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@VisibleForTesting
+@JvmField
+val TEST_ACTIVITY_STACK_TOKEN = Binder()
\ No newline at end of file
diff --git a/window/window-testing/src/main/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTesting.kt b/window/window-testing/src/main/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTesting.kt
index dd17311..4b39749 100644
--- a/window/window-testing/src/main/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTesting.kt
+++ b/window/window-testing/src/main/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTesting.kt
@@ -18,7 +18,6 @@
package androidx.window.testing.embedding
import android.content.res.Configuration
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.SplitAttributes
import androidx.window.embedding.SplitAttributesCalculatorParams
import androidx.window.embedding.SplitController
@@ -55,7 +54,6 @@
*
* @see SplitAttributesCalculatorParams
*/
-@OptIn(ExperimentalWindowApi::class)
@Suppress("FunctionName")
@JvmName("createTestSplitAttributesCalculatorParams")
@JvmOverloads
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 e3d3ac9..1ed7844 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,6 +17,7 @@
package androidx.window.testing.embedding
+import android.os.Binder
import androidx.window.embedding.ActivityStack
import androidx.window.embedding.SplitAttributes
import androidx.window.embedding.SplitInfo
@@ -42,4 +43,11 @@
primaryActivityStack: ActivityStack = TestActivityStack(),
secondActivityStack: ActivityStack = TestActivityStack(),
splitAttributes: SplitAttributes = SplitAttributes.Builder().build(),
-): SplitInfo = SplitInfo(primaryActivityStack, secondActivityStack, splitAttributes)
\ No newline at end of file
+): SplitInfo = SplitInfo(
+ primaryActivityStack,
+ secondActivityStack,
+ splitAttributes,
+ TEST_SPLIT_INFO_TOKEN
+)
+
+private val TEST_SPLIT_INFO_TOKEN = Binder()
\ No newline at end of file
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 3b77116..78aca27 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,8 +17,10 @@
package androidx.window.testing.embedding
import android.app.Activity
+import android.app.ActivityOptions
+import android.os.IBinder
import androidx.core.util.Consumer
-import androidx.window.core.ExperimentalWindowApi
+import androidx.window.embedding.ActivityStack
import androidx.window.embedding.EmbeddingBackend
import androidx.window.embedding.EmbeddingRule
import androidx.window.embedding.SplitAttributes
@@ -147,7 +149,6 @@
override fun isActivityEmbedded(activity: Activity): Boolean =
embeddedActivities.contains(activity)
- @ExperimentalWindowApi
override fun setSplitAttributesCalculator(
calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
) {
@@ -162,6 +163,37 @@
TODO("Not yet implemented")
}
+ override fun getActivityStack(activity: Activity): ActivityStack? {
+ TODO("Not yet implemented")
+ }
+
+ override fun setLaunchingActivityStack(
+ options: ActivityOptions,
+ token: IBinder
+ ): ActivityOptions {
+ TODO("Not yet implemented")
+ }
+
+ override fun finishActivityStacks(activityStacks: Set<ActivityStack>) {
+ TODO("Not yet implemented")
+ }
+
+ override fun isFinishActivityStacksSupported(): Boolean {
+ TODO("Not yet implemented")
+ }
+
+ override fun invalidateTopVisibleSplitAttributes() {
+ TODO("Not yet implemented")
+ }
+
+ override fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes) {
+ TODO("Not yet implemented")
+ }
+
+ override fun areSplitAttributesUpdatesSupported(): Boolean {
+ TODO("Not yet implemented")
+ }
+
private fun validateRules(rules: Set<EmbeddingRule>) {
val tags = HashSet<String>()
rules.forEach { rule ->
@@ -174,4 +206,4 @@
}
}
}
-}
\ No newline at end of file
+}
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..e946b53 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,8 @@
public void testActivityStackDefaultValue() {
final ActivityStack activityStack = TestActivityStack.createTestActivityStack();
- assertEquals(new ActivityStack(Collections.emptyList(), false /* isEmpty */),
- activityStack);
+ assertEquals(new ActivityStack(Collections.emptyList(), false /* isEmpty */,
+ TestActivityStack.TEST_ACTIVITY_STACK_TOKEN), activityStack);
}
/** Verifies {@link TestActivityStack} */
diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/ActivityStackTestingTest.kt b/window/window-testing/src/test/java/androidx/window/testing/embedding/ActivityStackTestingTest.kt
index 905bb8f..487aeab 100644
--- a/window/window-testing/src/test/java/androidx/window/testing/embedding/ActivityStackTestingTest.kt
+++ b/window/window-testing/src/test/java/androidx/window/testing/embedding/ActivityStackTestingTest.kt
@@ -34,7 +34,10 @@
fun testActivityStackDefaultValue() {
val activityStack = TestActivityStack()
- assertEquals(ActivityStack(emptyList(), isEmpty = false), activityStack)
+ assertEquals(
+ ActivityStack(emptyList(), isEmpty = false, TEST_ACTIVITY_STACK_TOKEN),
+ activityStack
+ )
}
/** Verifies [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 c2bb377..27e70c2 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
@@ -48,7 +48,6 @@
import java.util.List;
/** Test class to verify {@link TestSplitAttributesCalculatorParams} in Java. */
-@OptIn(markerClass = ExperimentalWindowApi.class)
@RunWith(RobolectricTestRunner.class)
public class SplitAttributesCalculatorParamsTestingJavaTest {
private static final Rect TEST_BOUNDS = new Rect(0, 0, 2000, 2000);
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 f8e8ce2..d81c721 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
@@ -37,7 +37,6 @@
import org.robolectric.RobolectricTestRunner
/** Test class to verify [TestSplitAttributesCalculatorParams]. */
-@OptIn(ExperimentalWindowApi::class)
@RunWith(RobolectricTestRunner::class)
class SplitAttributesCalculatorParamsTestingTest {
diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/StubEmbeddingBackendTest.kt b/window/window-testing/src/test/java/androidx/window/testing/embedding/StubEmbeddingBackendTest.kt
index 537e4c1..4afd0ce 100644
--- a/window/window-testing/src/test/java/androidx/window/testing/embedding/StubEmbeddingBackendTest.kt
+++ b/window/window-testing/src/test/java/androidx/window/testing/embedding/StubEmbeddingBackendTest.kt
@@ -33,7 +33,6 @@
@Test
fun removingSplitInfoListenerClearsListeners() {
- val backend = StubEmbeddingBackend()
val mockActivity = mock<Activity>()
val mockCallback = mock<Consumer<List<SplitInfo>>>()
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
new file mode 100644
index 0000000..8c261ef
--- /dev/null
+++ b/window/window-testing/src/test/java/androidx/window/testing/embedding/TestSplitInfo.kt
@@ -0,0 +1,74 @@
+/*
+ * 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,
+ token: IBinder = Binder()
+): ActivityStack {
+ return ActivityStack(
+ listOf(testActivity),
+ isEmpty,
+ token
+ )
+}
\ No newline at end of file
diff --git a/window/window-testing/src/test/resources/robolectric.properties b/window/window-testing/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..69fde47
--- /dev/null
+++ b/window/window-testing/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/window/window/api/api_lint.ignore b/window/window/api/api_lint.ignore
index 24bf744..d427dda 100644
--- a/window/window/api/api_lint.ignore
+++ b/window/window/api/api_lint.ignore
@@ -1,4 +1,6 @@
// Baseline format: 1.0
+ContextFirst: androidx.window.embedding.ActivityEmbeddingOptions#setLaunchingActivityStack(android.app.ActivityOptions, android.content.Context, androidx.window.embedding.ActivityStack) parameter #1:
+ Context is distinct, so it must be the first argument (method `windowLayoutInfoFlowable`)
GetterSetterNames: field ActivityRule.alwaysExpand:
Invalid name for boolean property `alwaysExpand`. Should start with one of `has`, `can`, `should`, `is`.
GetterSetterNames: field SplitAttributesCalculatorParams.areDefaultConstraintsSatisfied:
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index 8fbbb45..8a8b1b0 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -5,6 +5,92 @@
field public static final androidx.window.WindowProperties INSTANCE;
field public static final String PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE = "android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE";
field public static final String PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED = "android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED";
+ 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";
+ }
+
+}
+
+package androidx.window.area {
+
+ public final class WindowAreaCapability {
+ method public androidx.window.area.WindowAreaCapability.Operation getOperation();
+ method public androidx.window.area.WindowAreaCapability.Status getStatus();
+ property public final androidx.window.area.WindowAreaCapability.Operation operation;
+ property public final androidx.window.area.WindowAreaCapability.Status status;
+ }
+
+ public static final class WindowAreaCapability.Operation {
+ field public static final androidx.window.area.WindowAreaCapability.Operation.Companion Companion;
+ field public static final androidx.window.area.WindowAreaCapability.Operation OPERATION_PRESENT_ON_AREA;
+ field public static final androidx.window.area.WindowAreaCapability.Operation OPERATION_TRANSFER_ACTIVITY_TO_AREA;
+ }
+
+ public static final class WindowAreaCapability.Operation.Companion {
+ }
+
+ public static final class WindowAreaCapability.Status {
+ field public static final androidx.window.area.WindowAreaCapability.Status.Companion Companion;
+ field public static final androidx.window.area.WindowAreaCapability.Status WINDOW_AREA_STATUS_ACTIVE;
+ field public static final androidx.window.area.WindowAreaCapability.Status WINDOW_AREA_STATUS_AVAILABLE;
+ field public static final androidx.window.area.WindowAreaCapability.Status WINDOW_AREA_STATUS_UNAVAILABLE;
+ field public static final androidx.window.area.WindowAreaCapability.Status WINDOW_AREA_STATUS_UNSUPPORTED;
+ }
+
+ public static final class WindowAreaCapability.Status.Companion {
+ }
+
+ public interface WindowAreaController {
+ method public default static androidx.window.area.WindowAreaController getOrCreate();
+ method public kotlinx.coroutines.flow.Flow<java.util.List<androidx.window.area.WindowAreaInfo>> getWindowAreaInfos();
+ method public void presentContentOnWindowArea(android.os.Binder token, android.app.Activity activity, java.util.concurrent.Executor executor, androidx.window.area.WindowAreaPresentationSessionCallback windowAreaPresentationSessionCallback);
+ method public void transferActivityToWindowArea(android.os.Binder token, android.app.Activity activity, java.util.concurrent.Executor executor, androidx.window.area.WindowAreaSessionCallback windowAreaSessionCallback);
+ property public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.window.area.WindowAreaInfo>> windowAreaInfos;
+ field public static final androidx.window.area.WindowAreaController.Companion Companion;
+ }
+
+ public static final class WindowAreaController.Companion {
+ method public androidx.window.area.WindowAreaController getOrCreate();
+ }
+
+ public final class WindowAreaInfo {
+ method public androidx.window.area.WindowAreaSession? getActiveSession(androidx.window.area.WindowAreaCapability.Operation operation);
+ method public androidx.window.area.WindowAreaCapability? getCapability(androidx.window.area.WindowAreaCapability.Operation operation);
+ method public androidx.window.layout.WindowMetrics getMetrics();
+ method public android.os.Binder getToken();
+ method public androidx.window.area.WindowAreaInfo.Type getType();
+ method public void setMetrics(androidx.window.layout.WindowMetrics);
+ property public final androidx.window.layout.WindowMetrics metrics;
+ property public final android.os.Binder token;
+ property public final androidx.window.area.WindowAreaInfo.Type type;
+ }
+
+ public static final class WindowAreaInfo.Type {
+ field public static final androidx.window.area.WindowAreaInfo.Type.Companion Companion;
+ field public static final androidx.window.area.WindowAreaInfo.Type TYPE_REAR_FACING;
+ }
+
+ public static final class WindowAreaInfo.Type.Companion {
+ }
+
+ public interface WindowAreaPresentationSessionCallback {
+ method public void onContainerVisibilityChanged(boolean isVisible);
+ method public void onSessionEnded(Throwable? t);
+ method public void onSessionStarted(androidx.window.area.WindowAreaSessionPresenter session);
+ }
+
+ public interface WindowAreaSession {
+ method public void close();
+ }
+
+ public interface WindowAreaSessionCallback {
+ method public void onSessionEnded(Throwable? t);
+ method public void onSessionStarted(androidx.window.area.WindowAreaSession session);
+ }
+
+ public interface WindowAreaSessionPresenter extends androidx.window.area.WindowAreaSession {
+ method public android.content.Context getContext();
+ method public void setContentView(android.view.View view);
+ property public abstract android.content.Context context;
}
}
@@ -125,9 +211,27 @@
method public androidx.window.embedding.SplitAttributes.SplitType ratio(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float ratio);
}
+ public final class SplitAttributesCalculatorParams {
+ method public boolean getAreDefaultConstraintsSatisfied();
+ method public androidx.window.embedding.SplitAttributes getDefaultSplitAttributes();
+ method public android.content.res.Configuration getParentConfiguration();
+ method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();
+ method public androidx.window.layout.WindowMetrics getParentWindowMetrics();
+ method public String? getSplitRuleTag();
+ property public final boolean areDefaultConstraintsSatisfied;
+ property public final androidx.window.embedding.SplitAttributes defaultSplitAttributes;
+ property public final android.content.res.Configuration parentConfiguration;
+ property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;
+ property public final androidx.window.layout.WindowMetrics parentWindowMetrics;
+ property public final String? splitRuleTag;
+ }
+
public final class SplitController {
+ method public void clearSplitAttributesCalculator();
method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
method public androidx.window.embedding.SplitController.SplitSupportStatus getSplitSupportStatus();
+ method public boolean isSplitAttributesCalculatorSupported();
+ method 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);
property public final androidx.window.embedding.SplitController.SplitSupportStatus splitSupportStatus;
field public static final androidx.window.embedding.SplitController.Companion Companion;
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index 0c1d735..0f57b51 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -5,6 +5,92 @@
field public static final androidx.window.WindowProperties INSTANCE;
field public static final String PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE = "android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE";
field public static final String PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED = "android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED";
+ 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";
+ }
+
+}
+
+package androidx.window.area {
+
+ public final class WindowAreaCapability {
+ method public androidx.window.area.WindowAreaCapability.Operation getOperation();
+ method public androidx.window.area.WindowAreaCapability.Status getStatus();
+ property public final androidx.window.area.WindowAreaCapability.Operation operation;
+ property public final androidx.window.area.WindowAreaCapability.Status status;
+ }
+
+ public static final class WindowAreaCapability.Operation {
+ field public static final androidx.window.area.WindowAreaCapability.Operation.Companion Companion;
+ field public static final androidx.window.area.WindowAreaCapability.Operation OPERATION_PRESENT_ON_AREA;
+ field public static final androidx.window.area.WindowAreaCapability.Operation OPERATION_TRANSFER_ACTIVITY_TO_AREA;
+ }
+
+ public static final class WindowAreaCapability.Operation.Companion {
+ }
+
+ public static final class WindowAreaCapability.Status {
+ field public static final androidx.window.area.WindowAreaCapability.Status.Companion Companion;
+ field public static final androidx.window.area.WindowAreaCapability.Status WINDOW_AREA_STATUS_ACTIVE;
+ field public static final androidx.window.area.WindowAreaCapability.Status WINDOW_AREA_STATUS_AVAILABLE;
+ field public static final androidx.window.area.WindowAreaCapability.Status WINDOW_AREA_STATUS_UNAVAILABLE;
+ field public static final androidx.window.area.WindowAreaCapability.Status WINDOW_AREA_STATUS_UNSUPPORTED;
+ }
+
+ public static final class WindowAreaCapability.Status.Companion {
+ }
+
+ public interface WindowAreaController {
+ method public default static androidx.window.area.WindowAreaController getOrCreate();
+ method public kotlinx.coroutines.flow.Flow<java.util.List<androidx.window.area.WindowAreaInfo>> getWindowAreaInfos();
+ method public void presentContentOnWindowArea(android.os.Binder token, android.app.Activity activity, java.util.concurrent.Executor executor, androidx.window.area.WindowAreaPresentationSessionCallback windowAreaPresentationSessionCallback);
+ method public void transferActivityToWindowArea(android.os.Binder token, android.app.Activity activity, java.util.concurrent.Executor executor, androidx.window.area.WindowAreaSessionCallback windowAreaSessionCallback);
+ property public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.window.area.WindowAreaInfo>> windowAreaInfos;
+ field public static final androidx.window.area.WindowAreaController.Companion Companion;
+ }
+
+ public static final class WindowAreaController.Companion {
+ method public androidx.window.area.WindowAreaController getOrCreate();
+ }
+
+ public final class WindowAreaInfo {
+ method public androidx.window.area.WindowAreaSession? getActiveSession(androidx.window.area.WindowAreaCapability.Operation operation);
+ method public androidx.window.area.WindowAreaCapability? getCapability(androidx.window.area.WindowAreaCapability.Operation operation);
+ method public androidx.window.layout.WindowMetrics getMetrics();
+ method public android.os.Binder getToken();
+ method public androidx.window.area.WindowAreaInfo.Type getType();
+ method public void setMetrics(androidx.window.layout.WindowMetrics);
+ property public final androidx.window.layout.WindowMetrics metrics;
+ property public final android.os.Binder token;
+ property public final androidx.window.area.WindowAreaInfo.Type type;
+ }
+
+ public static final class WindowAreaInfo.Type {
+ field public static final androidx.window.area.WindowAreaInfo.Type.Companion Companion;
+ field public static final androidx.window.area.WindowAreaInfo.Type TYPE_REAR_FACING;
+ }
+
+ public static final class WindowAreaInfo.Type.Companion {
+ }
+
+ public interface WindowAreaPresentationSessionCallback {
+ method public void onContainerVisibilityChanged(boolean isVisible);
+ method public void onSessionEnded(Throwable? t);
+ method public void onSessionStarted(androidx.window.area.WindowAreaSessionPresenter session);
+ }
+
+ public interface WindowAreaSession {
+ method public void close();
+ }
+
+ public interface WindowAreaSessionCallback {
+ method public void onSessionEnded(Throwable? t);
+ method public void onSessionStarted(androidx.window.area.WindowAreaSession session);
+ }
+
+ public interface WindowAreaSessionPresenter extends androidx.window.area.WindowAreaSession {
+ method public android.content.Context getContext();
+ method public void setContentView(android.view.View view);
+ property public abstract android.content.Context context;
}
}
@@ -19,8 +105,11 @@
package androidx.window.embedding {
public final class ActivityEmbeddingController {
+ method @androidx.window.core.ExperimentalWindowApi public void finishActivityStacks(java.util.Set<androidx.window.embedding.ActivityStack> activityStacks);
+ method @androidx.window.core.ExperimentalWindowApi public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
method public static androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
method public boolean isActivityEmbedded(android.app.Activity activity);
+ method @androidx.window.core.ExperimentalWindowApi public boolean isFinishingActivityStacksSupported();
field public static final androidx.window.embedding.ActivityEmbeddingController.Companion Companion;
}
@@ -28,6 +117,12 @@
method public androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
}
+ public final class ActivityEmbeddingOptions {
+ method @androidx.window.core.ExperimentalWindowApi public static boolean isSetLaunchingActivityStackSupported(android.app.ActivityOptions);
+ method @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);
+ method @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.app.Activity activity);
+ }
+
public final class ActivityFilter {
ctor public ActivityFilter(android.content.ComponentName componentName, String? intentAction);
method public android.content.ComponentName getComponentName();
@@ -132,7 +227,7 @@
method public androidx.window.embedding.SplitAttributes.SplitType ratio(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float ratio);
}
- @androidx.window.core.ExperimentalWindowApi public final class SplitAttributesCalculatorParams {
+ public final class SplitAttributesCalculatorParams {
method public boolean getAreDefaultConstraintsSatisfied();
method public androidx.window.embedding.SplitAttributes getDefaultSplitAttributes();
method public android.content.res.Configuration getParentConfiguration();
@@ -149,14 +244,18 @@
public final class SplitController {
method @Deprecated @androidx.window.core.ExperimentalWindowApi public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
- method @androidx.window.core.ExperimentalWindowApi public void clearSplitAttributesCalculator();
+ method public void clearSplitAttributesCalculator();
method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
method public androidx.window.embedding.SplitController.SplitSupportStatus getSplitSupportStatus();
- method @androidx.window.core.ExperimentalWindowApi public boolean isSplitAttributesCalculatorSupported();
+ method @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();
+ method @androidx.window.core.ExperimentalWindowApi public boolean isInvalidatingTopVisibleSplitAttributesSupported();
+ method public boolean isSplitAttributesCalculatorSupported();
method @Deprecated @androidx.window.core.ExperimentalWindowApi public boolean isSplitSupported();
+ method @androidx.window.core.ExperimentalWindowApi public boolean isUpdatingSplitAttributesSupported();
method @Deprecated @androidx.window.core.ExperimentalWindowApi public void removeSplitListener(androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
- method @androidx.window.core.ExperimentalWindowApi public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
+ method 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 @androidx.window.core.ExperimentalWindowApi 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;
}
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index 8fbbb45..8a8b1b0 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -5,6 +5,92 @@
field public static final androidx.window.WindowProperties INSTANCE;
field public static final String PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE = "android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE";
field public static final String PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED = "android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED";
+ 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";
+ }
+
+}
+
+package androidx.window.area {
+
+ public final class WindowAreaCapability {
+ method public androidx.window.area.WindowAreaCapability.Operation getOperation();
+ method public androidx.window.area.WindowAreaCapability.Status getStatus();
+ property public final androidx.window.area.WindowAreaCapability.Operation operation;
+ property public final androidx.window.area.WindowAreaCapability.Status status;
+ }
+
+ public static final class WindowAreaCapability.Operation {
+ field public static final androidx.window.area.WindowAreaCapability.Operation.Companion Companion;
+ field public static final androidx.window.area.WindowAreaCapability.Operation OPERATION_PRESENT_ON_AREA;
+ field public static final androidx.window.area.WindowAreaCapability.Operation OPERATION_TRANSFER_ACTIVITY_TO_AREA;
+ }
+
+ public static final class WindowAreaCapability.Operation.Companion {
+ }
+
+ public static final class WindowAreaCapability.Status {
+ field public static final androidx.window.area.WindowAreaCapability.Status.Companion Companion;
+ field public static final androidx.window.area.WindowAreaCapability.Status WINDOW_AREA_STATUS_ACTIVE;
+ field public static final androidx.window.area.WindowAreaCapability.Status WINDOW_AREA_STATUS_AVAILABLE;
+ field public static final androidx.window.area.WindowAreaCapability.Status WINDOW_AREA_STATUS_UNAVAILABLE;
+ field public static final androidx.window.area.WindowAreaCapability.Status WINDOW_AREA_STATUS_UNSUPPORTED;
+ }
+
+ public static final class WindowAreaCapability.Status.Companion {
+ }
+
+ public interface WindowAreaController {
+ method public default static androidx.window.area.WindowAreaController getOrCreate();
+ method public kotlinx.coroutines.flow.Flow<java.util.List<androidx.window.area.WindowAreaInfo>> getWindowAreaInfos();
+ method public void presentContentOnWindowArea(android.os.Binder token, android.app.Activity activity, java.util.concurrent.Executor executor, androidx.window.area.WindowAreaPresentationSessionCallback windowAreaPresentationSessionCallback);
+ method public void transferActivityToWindowArea(android.os.Binder token, android.app.Activity activity, java.util.concurrent.Executor executor, androidx.window.area.WindowAreaSessionCallback windowAreaSessionCallback);
+ property public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.window.area.WindowAreaInfo>> windowAreaInfos;
+ field public static final androidx.window.area.WindowAreaController.Companion Companion;
+ }
+
+ public static final class WindowAreaController.Companion {
+ method public androidx.window.area.WindowAreaController getOrCreate();
+ }
+
+ public final class WindowAreaInfo {
+ method public androidx.window.area.WindowAreaSession? getActiveSession(androidx.window.area.WindowAreaCapability.Operation operation);
+ method public androidx.window.area.WindowAreaCapability? getCapability(androidx.window.area.WindowAreaCapability.Operation operation);
+ method public androidx.window.layout.WindowMetrics getMetrics();
+ method public android.os.Binder getToken();
+ method public androidx.window.area.WindowAreaInfo.Type getType();
+ method public void setMetrics(androidx.window.layout.WindowMetrics);
+ property public final androidx.window.layout.WindowMetrics metrics;
+ property public final android.os.Binder token;
+ property public final androidx.window.area.WindowAreaInfo.Type type;
+ }
+
+ public static final class WindowAreaInfo.Type {
+ field public static final androidx.window.area.WindowAreaInfo.Type.Companion Companion;
+ field public static final androidx.window.area.WindowAreaInfo.Type TYPE_REAR_FACING;
+ }
+
+ public static final class WindowAreaInfo.Type.Companion {
+ }
+
+ public interface WindowAreaPresentationSessionCallback {
+ method public void onContainerVisibilityChanged(boolean isVisible);
+ method public void onSessionEnded(Throwable? t);
+ method public void onSessionStarted(androidx.window.area.WindowAreaSessionPresenter session);
+ }
+
+ public interface WindowAreaSession {
+ method public void close();
+ }
+
+ public interface WindowAreaSessionCallback {
+ method public void onSessionEnded(Throwable? t);
+ method public void onSessionStarted(androidx.window.area.WindowAreaSession session);
+ }
+
+ public interface WindowAreaSessionPresenter extends androidx.window.area.WindowAreaSession {
+ method public android.content.Context getContext();
+ method public void setContentView(android.view.View view);
+ property public abstract android.content.Context context;
}
}
@@ -125,9 +211,27 @@
method public androidx.window.embedding.SplitAttributes.SplitType ratio(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float ratio);
}
+ public final class SplitAttributesCalculatorParams {
+ method public boolean getAreDefaultConstraintsSatisfied();
+ method public androidx.window.embedding.SplitAttributes getDefaultSplitAttributes();
+ method public android.content.res.Configuration getParentConfiguration();
+ method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();
+ method public androidx.window.layout.WindowMetrics getParentWindowMetrics();
+ method public String? getSplitRuleTag();
+ property public final boolean areDefaultConstraintsSatisfied;
+ property public final androidx.window.embedding.SplitAttributes defaultSplitAttributes;
+ property public final android.content.res.Configuration parentConfiguration;
+ property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;
+ property public final androidx.window.layout.WindowMetrics parentWindowMetrics;
+ property public final String? splitRuleTag;
+ }
+
public final class SplitController {
+ method public void clearSplitAttributesCalculator();
method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
method public androidx.window.embedding.SplitController.SplitSupportStatus getSplitSupportStatus();
+ method public boolean isSplitAttributesCalculatorSupported();
+ method 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);
property public final androidx.window.embedding.SplitController.SplitSupportStatus splitSupportStatus;
field public static final androidx.window.embedding.SplitController.Companion Companion;
diff --git a/window/window/build.gradle b/window/window/build.gradle
index 4e4170e..ba28888a 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -49,11 +49,9 @@
implementation("androidx.collection:collection:1.1.0")
implementation("androidx.core:core:1.8.0")
- def extensions_core_version = "androidx.window.extensions.core:core:1.0.0-rc01"
- def extensions_version = "androidx.window.extensions:extensions:1.1.0-rc01"
- implementation(extensions_core_version)
+ implementation("androidx.window.extensions.core:core:1.0.0-beta01")
compileOnly(project(":window:sidecar:sidecar"))
- compileOnly(extensions_version)
+ compileOnly(project(":window:extensions:extensions"))
testImplementation(libs.testCore)
testImplementation(libs.testRunner)
@@ -63,9 +61,9 @@
testImplementation(libs.mockitoCore4)
testImplementation(libs.mockitoKotlin4)
testImplementation(libs.kotlinCoroutinesTest)
- testImplementation(extensions_core_version)
+ testImplementation("androidx.window.extensions.core:core:1.0.0-beta01")
testImplementation(compileOnly(project(":window:sidecar:sidecar")))
- testImplementation(compileOnly(extensions_version))
+ testImplementation(compileOnly(project(":window:extensions:extensions")))
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.kotlinTestJunit)
@@ -79,13 +77,13 @@
androidTestImplementation(libs.multidex)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.junit) // Needed for Assert.assertThrows
- androidTestImplementation(extensions_core_version)
+ androidTestImplementation("androidx.window.extensions.core:core:1.0.0-beta01")
androidTestImplementation(compileOnly(project(":window:sidecar:sidecar")))
- androidTestImplementation(compileOnly(extensions_version))
+ androidTestImplementation(compileOnly(project(":window:extensions:extensions")))
}
androidx {
- name = "Jetpack WindowManager Library"
+ name = "WindowManager"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2020"
description = "WindowManager Jetpack library. Currently only provides additional " +
diff --git a/window/window/proguard-rules.pro b/window/window/proguard-rules.pro
index 609e1cc1..b8cf236 100644
--- a/window/window/proguard-rules.pro
+++ b/window/window/proguard-rules.pro
@@ -22,4 +22,6 @@
androidx.window.layout.adapter.sidecar.DistinctElementSidecarCallback {
public *** onDeviceStateChanged(androidx.window.sidecar.SidecarDeviceState);
public *** onWindowLayoutChanged(android.os.IBinder, androidx.window.sidecar.SidecarWindowLayoutInfo);
-}
\ No newline at end of file
+}
+# Required for window area API reflection guard
+-keep interface androidx.window.area.reflectionguard.* {*;}
\ No newline at end of file
diff --git a/window/window/samples/build.gradle b/window/window/samples/build.gradle
index e44e0ac..7db715c 100644
--- a/window/window/samples/build.gradle
+++ b/window/window/samples/build.gradle
@@ -36,7 +36,7 @@
}
androidx {
- name = "Jetpack WindowManager Library Samples"
+ name = "WindowManager Samples"
type = LibraryType.SAMPLES
inceptionYear = "2022"
description = "Code samples for WindowManager Jetpack library."
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..a7ab9cb
--- /dev/null
+++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/FinishActivityStacksSamples.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.core.ExperimentalWindowApi
+import androidx.window.embedding.ActivityEmbeddingController
+import androidx.window.embedding.SplitController
+
+@OptIn(ExperimentalWindowApi::class)
+@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/SplitAttributesCalculatorSamples.kt b/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
index c6a3607..e244fb9 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
@@ -18,7 +18,6 @@
import android.app.Application
import androidx.annotation.Sampled
-import androidx.window.core.ExperimentalWindowApi
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
@@ -26,7 +25,6 @@
import androidx.window.embedding.SplitController
import androidx.window.layout.FoldingFeature
-@OptIn(ExperimentalWindowApi::class)
@Sampled
fun splitAttributesCalculatorSample() {
SplitController.getInstance(context)
@@ -79,7 +77,6 @@
}
}
-@OptIn(ExperimentalWindowApi::class)
@Sampled
fun splitWithOrientations() {
SplitController.getInstance(context)
@@ -107,7 +104,6 @@
}
}
-@OptIn(ExperimentalWindowApi::class)
@Sampled
fun expandContainersInPortrait() {
SplitController.getInstance(context)
@@ -135,7 +131,6 @@
}
}
-@OptIn(ExperimentalWindowApi::class)
@Sampled
fun fallbackToExpandContainersForSplitTypeHinge() {
SplitController.getInstance(context).setSplitAttributesCalculator { params ->
diff --git a/window/window/src/androidTest/AndroidManifest.xml b/window/window/src/androidTest/AndroidManifest.xml
index cd1d81e..40b0da7 100644
--- a/window/window/src/androidTest/AndroidManifest.xml
+++ b/window/window/src/androidTest/AndroidManifest.xml
@@ -28,5 +28,11 @@
<property
android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
android:value="true" />
+
+ <!-- Compat property -->
+ <property
+ android:name=
+ "android.window.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED"
+ 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 34eb50f..51be602 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowPropertiesTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/WindowPropertiesTest.kt
@@ -22,6 +22,7 @@
import androidx.annotation.RequiresApi
import androidx.test.ext.junit.rules.ActivityScenarioRule
import org.junit.Assert.assertTrue
+import org.junit.Assert.assertFalse
import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.Test
@@ -67,6 +68,25 @@
}
}
+ @Test
+ fun test_property_allow_ignoring_orientation_request_when_loop_detected() {
+ assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ // 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_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED
+ )
+ )
+ }
+ }
+
@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/area/SafeWindowAreaComponentProviderTest.kt b/window/window/src/androidTest/java/androidx/window/area/SafeWindowAreaComponentProviderTest.kt
new file mode 100644
index 0000000..50bb347
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/area/SafeWindowAreaComponentProviderTest.kt
@@ -0,0 +1,55 @@
+/*
+ * 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
+
+import androidx.window.core.ExtensionsUtil
+import androidx.window.extensions.WindowExtensionsProvider
+import org.junit.Assert.assertNull
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+
+/**
+ * An integration test to verify that if [WindowExtensionsProvider] is present then
+ * [SafeWindowAreaComponentProvider.windowAreaComponent] will return a value. This can fail if
+ * the implementation of window:extensions:extensions does not have the expected API.
+ */
+class SafeWindowAreaComponentProviderTest {
+
+ /**
+ * Test that if [WindowExtensionsProvider] is available then
+ * [SafeWindowAreaComponentProvider.windowAreaComponent] returns a non-null value.
+ */
+ @Test
+ fun windowAreaComponentIsAvailable_ifProviderIsAvailable() {
+ assumeTrue(ExtensionsUtil.safeVendorApiLevel >= 2)
+ 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)
+ }
+ // TODO(b/267831038): verify upon each api level
+ // TODO(b/267708462): more reliable test for testing actual method matching
+ } catch (e: UnsupportedOperationException) {
+ // Invalid implementation of extensions
+ assertNull(safeComponent)
+ }
+ }
+}
\ No newline at end of file
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 e796c8f..e5e4d62 100644
--- a/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
@@ -16,29 +16,51 @@
package androidx.window.area
-import android.annotation.TargetApi
import android.app.Activity
+import android.content.Context
import android.content.pm.ActivityInfo
+import android.os.Binder
import android.os.Build
+import android.util.DisplayMetrics
+import android.view.View
+import android.widget.TextView
import androidx.annotation.RequiresApi
+import androidx.core.view.WindowInsetsCompat
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.window.TestActivity
-import androidx.window.TestConsumer
-import androidx.window.core.ExperimentalWindowApi
-import androidx.window.extensions.area.WindowAreaComponent
-import androidx.window.extensions.core.util.function.Consumer
import androidx.window.WindowTestUtils.Companion.assumeAtLeastVendorApiLevel
+import androidx.window.area.WindowAreaCapability.Operation.Companion.OPERATION_PRESENT_ON_AREA
+import androidx.window.area.WindowAreaCapability.Operation.Companion.OPERATION_TRANSFER_ACTIVITY_TO_AREA
+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.core.Bounds
+import androidx.window.extensions.area.ExtensionWindowAreaPresentation
+import androidx.window.extensions.area.ExtensionWindowAreaStatus
+import androidx.window.extensions.area.WindowAreaComponent
+import androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_ACTIVE
+import androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_INACTIVE
+import androidx.window.extensions.area.WindowAreaComponent.STATUS_ACTIVE
+import androidx.window.extensions.area.WindowAreaComponent.STATUS_AVAILABLE
+import androidx.window.extensions.area.WindowAreaComponent.STATUS_UNAVAILABLE
+import androidx.window.extensions.area.WindowAreaComponent.STATUS_UNSUPPORTED
+import androidx.window.extensions.core.util.function.Consumer
+import androidx.window.layout.WindowMetrics
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.launch
-import org.junit.Rule
-import org.junit.Test
-import kotlin.test.assertFailsWith
import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
-@OptIn(ExperimentalCoroutinesApi::class, ExperimentalWindowApi::class)
+@OptIn(ExperimentalCoroutinesApi::class)
class WindowAreaControllerImplTest {
@get:Rule
@@ -47,55 +69,107 @@
private val testScope = TestScope(UnconfinedTestDispatcher())
- @TargetApi(Build.VERSION_CODES.N)
+ /**
+ * Tests that we can get a list of [WindowAreaInfo] objects with a type of
+ * [WindowAreaInfo.Type.TYPE_REAR_FACING]. Verifies that updating the status of features on
+ * device returns an updated [WindowAreaInfo] list.
+ */
+ @RequiresApi(Build.VERSION_CODES.Q)
@Test
- public fun testRearDisplayStatus(): Unit = testScope.runTest {
+ public fun testRearFacingWindowAreaInfoList(): Unit = testScope.runTest {
+ assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.Q)
assumeAtLeastVendorApiLevel(2)
activityScenario.scenario.onActivity {
val extensionComponent = FakeWindowAreaComponent()
- val repo = WindowAreaControllerImpl(extensionComponent)
- val collector = TestConsumer<WindowAreaStatus>()
- extensionComponent
- .updateStatusListeners(WindowAreaComponent.STATUS_UNAVAILABLE)
- testScope.launch(Job()) {
- repo.rearDisplayStatus().collect(collector::accept)
- }
- collector.assertValue(WindowAreaStatus.UNAVAILABLE)
- extensionComponent
- .updateStatusListeners(WindowAreaComponent.STATUS_AVAILABLE)
- collector.assertValues(
- WindowAreaStatus.UNAVAILABLE,
- WindowAreaStatus.AVAILABLE
+ val controller = WindowAreaControllerImpl(
+ windowAreaComponent = extensionComponent,
+ vendorApiLevel = 2
)
+ extensionComponent.currentRearDisplayStatus = STATUS_UNAVAILABLE
+ val collector = TestWindowAreaInfoListConsumer()
+ testScope.launch(Job()) {
+ controller.windowAreaInfos.collect(collector::accept)
+ }
+
+ val expectedAreaInfo = WindowAreaInfo(
+ metrics = createEmptyWindowMetrics(),
+ type = WindowAreaInfo.Type.TYPE_REAR_FACING,
+ token = Binder(REAR_FACING_BINDER_DESCRIPTION),
+ windowAreaComponent = extensionComponent
+ )
+ val rearDisplayCapability = WindowAreaCapability(
+ OPERATION_TRANSFER_ACTIVITY_TO_AREA,
+ WINDOW_AREA_STATUS_UNAVAILABLE
+ )
+ expectedAreaInfo.capabilityMap[OPERATION_TRANSFER_ACTIVITY_TO_AREA] =
+ rearDisplayCapability
+
+ assertEquals(1, collector.values.size)
+ assertEquals(listOf(expectedAreaInfo), collector.values[0])
+
+ extensionComponent
+ .updateRearDisplayStatusListeners(STATUS_AVAILABLE)
+
+ val updatedAreaInfo = WindowAreaInfo(
+ metrics = createEmptyWindowMetrics(),
+ type = WindowAreaInfo.Type.TYPE_REAR_FACING,
+ token = Binder(REAR_FACING_BINDER_DESCRIPTION),
+ windowAreaComponent = extensionComponent
+ )
+ val updatedRearDisplayCapability = WindowAreaCapability(
+ OPERATION_TRANSFER_ACTIVITY_TO_AREA,
+ WINDOW_AREA_STATUS_AVAILABLE
+ )
+ updatedAreaInfo.capabilityMap[OPERATION_TRANSFER_ACTIVITY_TO_AREA] =
+ updatedRearDisplayCapability
+
+ assertEquals(2, collector.values.size)
+ assertEquals(listOf(updatedAreaInfo), collector.values[1])
}
}
@Test
- public fun testRearDisplayStatusNullComponent(): Unit = testScope.runTest {
+ public fun testWindowAreaInfoListNullComponent(): Unit = testScope.runTest {
activityScenario.scenario.onActivity {
- val repo = EmptyWindowAreaControllerImpl()
- val collector = TestConsumer<WindowAreaStatus>()
+ val controller = EmptyWindowAreaControllerImpl()
+ val collector = TestWindowAreaInfoListConsumer()
testScope.launch(Job()) {
- repo.rearDisplayStatus().collect(collector::accept)
+ controller.windowAreaInfos.collect(collector::accept)
}
- collector.assertValue(WindowAreaStatus.UNSUPPORTED)
+ assertTrue(collector.values.size == 1)
+ assertEquals(listOf(), collector.values[0])
}
}
/**
- * Tests the rear display mode flow works as expected. Tests the flow
+ * Tests the transfer to rear facing window area flow. Tests the flow
* through WindowAreaControllerImpl with a fake extension. This fake extension
- * changes the orientation of the activity to landscape when rear display mode is enabled
- * and then returns it back to portrait when it's disabled.
+ * changes the orientation of the activity to landscape to simulate a configuration change that
+ * would occur when transferring to the rear facing window area and then returns it back to
+ * portrait when it's disabled.
*/
- @TargetApi(Build.VERSION_CODES.N)
+ @RequiresApi(Build.VERSION_CODES.Q)
@Test
- public fun testRearDisplayMode(): Unit = testScope.runTest {
+ public fun testTransferToRearFacingWindowArea(): Unit = testScope.runTest {
assumeAtLeastVendorApiLevel(2)
val extensions = FakeWindowAreaComponent()
- val repo = WindowAreaControllerImpl(extensions)
- extensions.currentStatus = WindowAreaComponent.STATUS_AVAILABLE
+ val controller = WindowAreaControllerImpl(
+ windowAreaComponent = extensions,
+ vendorApiLevel = 2
+ )
+ extensions.currentRearDisplayStatus = STATUS_AVAILABLE
val callback = TestWindowAreaSessionCallback()
+ var windowAreaInfo: WindowAreaInfo? = null
+ testScope.launch(Job()) {
+ windowAreaInfo = controller.windowAreaInfos.firstOrNull()
+ ?.firstOrNull { it.type == WindowAreaInfo.Type.TYPE_REAR_FACING }
+ }
+ assertNotNull(windowAreaInfo)
+ assertEquals(
+ windowAreaInfo!!.getCapability(OPERATION_TRANSFER_ACTIVITY_TO_AREA)?.status,
+ WINDOW_AREA_STATUS_AVAILABLE
+ )
+
activityScenario.scenario.onActivity { testActivity ->
testActivity.resetLayoutCounter()
testActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
@@ -105,7 +179,12 @@
activityScenario.scenario.onActivity { testActivity ->
assert(testActivity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
testActivity.resetLayoutCounter()
- repo.rearDisplayMode(testActivity, Runnable::run, callback)
+ controller.transferActivityToWindowArea(
+ windowAreaInfo!!.token,
+ testActivity,
+ Runnable::run,
+ callback
+ )
}
activityScenario.scenario.onActivity { testActivity ->
@@ -120,84 +199,255 @@
}
}
- @TargetApi(Build.VERSION_CODES.N)
+ @RequiresApi(Build.VERSION_CODES.Q)
@Test
- public fun testRearDisplayModeReturnsError(): Unit = testScope.runTest {
+ public fun testTransferRearDisplayReturnsError_statusUnavailable(): Unit = testScope.runTest {
+ testTransferRearDisplayReturnsError(STATUS_UNAVAILABLE)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.Q)
+ @Test
+ public fun testTransferRearDisplayReturnsError_statusActive(): Unit = testScope.runTest {
+ testTransferRearDisplayReturnsError(STATUS_ACTIVE)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.Q)
+ private fun testTransferRearDisplayReturnsError(
+ initialState: @WindowAreaComponent.WindowAreaStatus Int
+ ) {
assumeAtLeastVendorApiLevel(2)
- val extensionComponent = FakeWindowAreaComponent()
- extensionComponent.currentStatus = WindowAreaComponent.STATUS_UNAVAILABLE
- val repo = WindowAreaControllerImpl(extensionComponent)
+ val extensions = FakeWindowAreaComponent()
+ val controller = WindowAreaControllerImpl(
+ windowAreaComponent = extensions,
+ vendorApiLevel = 2
+ )
+ extensions.currentRearDisplayStatus = initialState
val callback = TestWindowAreaSessionCallback()
+ var windowAreaInfo: WindowAreaInfo? = null
+ testScope.launch(Job()) {
+ windowAreaInfo = controller.windowAreaInfos.firstOrNull()
+ ?.firstOrNull { it.type == WindowAreaInfo.Type.TYPE_REAR_FACING }
+ }
+ assertNotNull(windowAreaInfo)
+ assertEquals(
+ windowAreaInfo!!.getCapability(OPERATION_TRANSFER_ACTIVITY_TO_AREA)?.status,
+ WindowAreaAdapter.translate(initialState)
+ )
+
activityScenario.scenario.onActivity { testActivity ->
- assertFailsWith(
- exceptionClass = UnsupportedOperationException::class,
- block = { repo.rearDisplayMode(testActivity, Runnable::run, callback) }
+ controller.transferActivityToWindowArea(
+ windowAreaInfo!!.token,
+ testActivity,
+ Runnable::run,
+ callback
)
+ assertNotNull(callback.error)
+ assertNull(callback.currentSession)
}
}
- @TargetApi(Build.VERSION_CODES.N)
+ /**
+ * Tests the presentation flow on to a rear facing display works as expected. The
+ * [WindowAreaPresentationSessionCallback] provided to
+ * [WindowAreaControllerImpl.presentContentOnWindowArea] should receive a
+ * [WindowAreaSessionPresenter] when the session is active, and be notified that the [View]
+ * provided through [WindowAreaSessionPresenter.setContentView] is visible when inflated.
+ *
+ * Tests the flow through WindowAreaControllerImpl with a fake extension component.
+ */
+ @RequiresApi(Build.VERSION_CODES.Q)
@Test
- public fun testRearDisplayModeNullComponent(): Unit = testScope.runTest {
- val repo = EmptyWindowAreaControllerImpl()
- val callback = TestWindowAreaSessionCallback()
+ public fun testRearDisplayPresentationMode(): Unit = testScope.runTest {
+ assumeAtLeastVendorApiLevel(3)
+ val extensions = FakeWindowAreaComponent()
+ val controller = WindowAreaControllerImpl(
+ windowAreaComponent = extensions,
+ vendorApiLevel = 3
+ )
+ var windowAreaInfo: WindowAreaInfo? = null
+ extensions.updateRearDisplayStatusListeners(STATUS_AVAILABLE)
+ extensions.updateRearDisplayPresentationStatusListeners(STATUS_AVAILABLE)
+ testScope.launch(Job()) {
+ windowAreaInfo = controller.windowAreaInfos.firstOrNull()
+ ?.firstOrNull { it.type == WindowAreaInfo.Type.TYPE_REAR_FACING }
+ }
+ assertNotNull(windowAreaInfo)
+ assertTrue {
+ windowAreaInfo!!
+ .getCapability(OPERATION_PRESENT_ON_AREA)?.status ==
+ WINDOW_AREA_STATUS_AVAILABLE
+ }
+
+ val callback = TestWindowAreaPresentationSessionCallback()
activityScenario.scenario.onActivity { testActivity ->
- assertFailsWith(
- exceptionClass = UnsupportedOperationException::class,
- block = { repo.rearDisplayMode(testActivity, Runnable::run, callback) }
+ controller.presentContentOnWindowArea(
+ windowAreaInfo!!.token,
+ testActivity,
+ Runnable::run,
+ callback
)
+ assert(callback.sessionActive)
+ assert(!callback.contentVisible)
+
+ callback.presentation?.setContentView(TextView(testActivity))
+ assert(callback.contentVisible)
+ assert(callback.sessionActive)
+
+ callback.presentation?.close()
+ assert(!callback.contentVisible)
+ assert(!callback.sessionActive)
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.Q)
+ @Test
+ public fun testRearDisplayPresentationModeSessionEndedError(): Unit = testScope.runTest {
+ assumeAtLeastVendorApiLevel(3)
+ val extensionComponent = FakeWindowAreaComponent()
+ val controller = WindowAreaControllerImpl(
+ windowAreaComponent = extensionComponent,
+ vendorApiLevel = 3
+ )
+ var windowAreaInfo: WindowAreaInfo? = null
+ extensionComponent.updateRearDisplayStatusListeners(STATUS_UNAVAILABLE)
+ extensionComponent.updateRearDisplayPresentationStatusListeners(STATUS_UNAVAILABLE)
+ testScope.launch(Job()) {
+ windowAreaInfo = controller.windowAreaInfos.firstOrNull()
+ ?.firstOrNull { it.type == WindowAreaInfo.Type.TYPE_REAR_FACING }
+ }
+ assertNotNull(windowAreaInfo)
+ assertTrue {
+ windowAreaInfo!!
+ .getCapability(OPERATION_PRESENT_ON_AREA)?.status ==
+ WINDOW_AREA_STATUS_UNAVAILABLE
+ }
+
+ val callback = TestWindowAreaPresentationSessionCallback()
+ activityScenario.scenario.onActivity { testActivity ->
+ controller.presentContentOnWindowArea(
+ windowAreaInfo!!.token,
+ testActivity,
+ Runnable::run,
+ callback
+ )
+ assert(!callback.sessionActive)
+ assert(callback.sessionError != null)
+ assert(callback.sessionError is IllegalStateException)
+ }
+ }
+
+ private fun createEmptyWindowMetrics(): WindowMetrics {
+ val displayMetrics = DisplayMetrics()
+ return WindowMetrics(
+ Bounds(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels),
+ WindowInsetsCompat.Builder().build()
+ )
+ }
+
+ private class TestWindowAreaInfoListConsumer : Consumer<List<WindowAreaInfo>> {
+
+ val values: MutableList<List<WindowAreaInfo>> = mutableListOf()
+ override fun accept(infos: List<WindowAreaInfo>) {
+ values.add(infos)
}
}
private class FakeWindowAreaComponent : WindowAreaComponent {
- val statusListeners = mutableListOf<Consumer<Int>>()
- var currentStatus = WindowAreaComponent.STATUS_UNSUPPORTED
- var testActivity: Activity? = null
- var sessionConsumer: Consumer<Int>? = null
+ val rearDisplayStatusListeners = mutableListOf<Consumer<Int>>()
+ val rearDisplayPresentationStatusListeners =
+ mutableListOf<Consumer<ExtensionWindowAreaStatus>>()
+ var currentRearDisplayStatus = STATUS_UNSUPPORTED
+ var currentRearDisplayPresentationStatus = STATUS_UNSUPPORTED
- @RequiresApi(Build.VERSION_CODES.N)
+ var testActivity: Activity? = null
+ var rearDisplaySessionConsumer: Consumer<Int>? = null
+ var rearDisplayPresentationSessionConsumer: Consumer<Int>? = null
+
override fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
- statusListeners.add(consumer)
- consumer.accept(currentStatus)
+ rearDisplayStatusListeners.add(consumer)
+ consumer.accept(currentRearDisplayStatus)
}
override fun removeRearDisplayStatusListener(consumer: Consumer<Int>) {
- statusListeners.remove(consumer)
+ rearDisplayStatusListeners.remove(consumer)
+ }
+
+ override fun addRearDisplayPresentationStatusListener(
+ consumer: Consumer<ExtensionWindowAreaStatus>
+ ) {
+ rearDisplayPresentationStatusListeners.add(consumer)
+ consumer.accept(TestExtensionWindowAreaStatus(currentRearDisplayPresentationStatus))
+ }
+
+ override fun removeRearDisplayPresentationStatusListener(
+ consumer: Consumer<ExtensionWindowAreaStatus>
+ ) {
+ rearDisplayPresentationStatusListeners.remove(consumer)
}
// Fake WindowAreaComponent will change the orientation of the activity to signal
// entering rear display mode, as well as ending the session
- @RequiresApi(Build.VERSION_CODES.N)
override fun startRearDisplaySession(
activity: Activity,
rearDisplaySessionConsumer: Consumer<Int>
) {
- if (currentStatus != WindowAreaComponent.STATUS_AVAILABLE) {
- throw UnsupportedOperationException("Rear Display mode cannot be enabled currently")
+ if (currentRearDisplayStatus != STATUS_AVAILABLE) {
+ rearDisplaySessionConsumer.accept(SESSION_STATE_INACTIVE)
}
testActivity = activity
- sessionConsumer = rearDisplaySessionConsumer
+ this.rearDisplaySessionConsumer = rearDisplaySessionConsumer
testActivity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
rearDisplaySessionConsumer.accept(WindowAreaComponent.SESSION_STATE_ACTIVE)
}
- @RequiresApi(Build.VERSION_CODES.N)
override fun endRearDisplaySession() {
testActivity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
- sessionConsumer?.accept(WindowAreaComponent.SESSION_STATE_INACTIVE)
+ rearDisplaySessionConsumer?.accept(SESSION_STATE_INACTIVE)
}
- @RequiresApi(Build.VERSION_CODES.N)
- fun updateStatusListeners(newStatus: Int) {
- currentStatus = newStatus
- for (consumer in statusListeners) {
- consumer.accept(currentStatus)
+ override fun startRearDisplayPresentationSession(
+ activity: Activity,
+ consumer: Consumer<Int>
+ ) {
+ if (currentRearDisplayPresentationStatus != STATUS_AVAILABLE) {
+ consumer.accept(SESSION_STATE_INACTIVE)
+ return
+ }
+ testActivity = activity
+ rearDisplayPresentationSessionConsumer = consumer
+ consumer.accept(SESSION_STATE_ACTIVE)
+ }
+
+ override fun endRearDisplayPresentationSession() {
+ rearDisplayPresentationSessionConsumer?.accept(
+ WindowAreaComponent.SESSION_STATE_CONTENT_INVISIBLE)
+ rearDisplayPresentationSessionConsumer?.accept(
+ WindowAreaComponent.SESSION_STATE_INACTIVE)
+ }
+
+ override fun getRearDisplayPresentation(): ExtensionWindowAreaPresentation? {
+ return TestExtensionWindowAreaPresentation(
+ testActivity!!,
+ rearDisplayPresentationSessionConsumer!!
+ )
+ }
+
+ fun updateRearDisplayStatusListeners(newStatus: Int) {
+ currentRearDisplayStatus = newStatus
+ for (consumer in rearDisplayStatusListeners) {
+ consumer.accept(currentRearDisplayStatus)
+ }
+ }
+
+ fun updateRearDisplayPresentationStatusListeners(newStatus: Int) {
+ currentRearDisplayPresentationStatus = newStatus
+ for (consumer in rearDisplayPresentationStatusListeners) {
+ consumer.accept(TestExtensionWindowAreaStatus(currentRearDisplayPresentationStatus))
}
}
}
private class TestWindowAreaSessionCallback : WindowAreaSessionCallback {
-
var currentSession: WindowAreaSession? = null
var error: Throwable? = null
@@ -205,10 +455,61 @@
currentSession = session
}
- override fun onSessionEnded() {
+ override fun onSessionEnded(t: Throwable?) {
+ error = t
currentSession = null
}
fun endSession() = currentSession?.close()
}
-}
+
+ private class TestWindowAreaPresentationSessionCallback :
+ WindowAreaPresentationSessionCallback {
+ var sessionActive: Boolean = false
+ var contentVisible: Boolean = false
+ var presentation: WindowAreaSessionPresenter? = null
+ var sessionError: Throwable? = null
+ override fun onSessionStarted(session: WindowAreaSessionPresenter) {
+ sessionActive = true
+ presentation = session
+ }
+
+ override fun onSessionEnded(t: Throwable?) {
+ presentation = null
+ sessionActive = false
+ sessionError = t
+ }
+
+ override fun onContainerVisibilityChanged(isVisible: Boolean) {
+ contentVisible = isVisible
+ }
+ }
+
+ private class TestExtensionWindowAreaStatus(private val status: Int) :
+ ExtensionWindowAreaStatus {
+ override fun getWindowAreaStatus(): Int {
+ return status
+ }
+
+ override fun getWindowAreaDisplayMetrics(): DisplayMetrics {
+ return DisplayMetrics()
+ }
+ }
+
+ private class TestExtensionWindowAreaPresentation(
+ private val activity: Activity,
+ private val sessionConsumer: Consumer<Int>
+ ) : ExtensionWindowAreaPresentation {
+ override fun getPresentationContext(): Context {
+ return activity
+ }
+
+ override fun setPresentationView(view: View) {
+ sessionConsumer.accept(WindowAreaComponent.SESSION_STATE_CONTENT_VISIBLE)
+ }
+ }
+
+ companion object {
+ private const val REAR_FACING_BINDER_DESCRIPTION = "TEST_WINDOW_AREA_REAR_FACING"
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..b385670
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt
@@ -0,0 +1,257 @@
+/*
+ * 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 android.content.Context
+import android.util.DisplayMetrics
+import android.view.View
+import androidx.window.extensions.area.ExtensionWindowAreaPresentation
+import androidx.window.extensions.area.ExtensionWindowAreaStatus
+import androidx.window.extensions.area.WindowAreaComponent
+import androidx.window.extensions.core.util.function.Consumer
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+
+/**
+ * Unit test for [WindowAreaComponentValidator]
+ */
+class WindowAreaComponentValidatorTest {
+
+ /**
+ * Test that validator returns true if the component fully implements [WindowAreaComponent]
+ */
+ @Test
+ fun isWindowAreaComponentValid_fullImplementation() {
+ assertTrue(
+ WindowAreaComponentValidator.isWindowAreaComponentValid(
+ WindowAreaComponentFullImplementation::class.java, 2))
+ assertTrue(
+ WindowAreaComponentValidator.isWindowAreaComponentValid(
+ WindowAreaComponentFullImplementation::class.java, 3))
+ }
+
+ /**
+ * Test that validator returns correct results for API Level 2 [WindowAreaComponent]
+ * implementation.
+ */
+ @Test
+ fun isWindowAreaComponentValid_apiLevel2() {
+ assertTrue(
+ WindowAreaComponentValidator.isWindowAreaComponentValid(
+ WindowAreaComponentApiV2Implementation::class.java, 2))
+ assertFalse(
+ WindowAreaComponentValidator.isWindowAreaComponentValid(
+ IncompleteWindowAreaComponentApiV2Implementation::class.java, 3))
+ }
+
+ /**
+ * Test that validator returns correct results for API Level 3 [WindowAreaComponent]
+ * implementation.
+ */
+ @Test
+ fun isWindowAreaComponentValid_apiLevel3() {
+ assertTrue(
+ WindowAreaComponentValidator.isWindowAreaComponentValid(
+ WindowAreaComponentApiV3Implementation::class.java, 2))
+ assertTrue(
+ WindowAreaComponentValidator.isWindowAreaComponentValid(
+ WindowAreaComponentApiV3Implementation::class.java, 3))
+ }
+
+ /**
+ * Test that validator returns false if the component implementation is incomplete
+ */
+ @Test
+ fun isWindowAreaComponentValid_falseIfIncompleteImplementation() {
+ assertFalse(
+ WindowAreaComponentValidator.isWindowAreaComponentValid(
+ IncompleteWindowAreaComponentApiV2Implementation::class.java, 2))
+ }
+
+ /**
+ * Test that validator returns true if the [ExtensionWindowAreaStatus] is valid
+ */
+ @Test
+ fun isExtensionWindowAreaStatusValid_trueIfValid() {
+ assertTrue(
+ WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
+ ValidExtensionWindowAreaStatus::class.java, 2))
+ assertTrue(
+ WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
+ ValidExtensionWindowAreaStatus::class.java, 3))
+ }
+
+ /**
+ * Test that validator returns false if the [ExtensionWindowAreaStatus] is incomplete
+ */
+ @Test
+ fun isExtensionWindowAreaStatusValid_falseIfIncomplete() {
+ assertFalse(
+ WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
+ IncompleteExtensionWindowAreaStatus::class.java, 2))
+ assertFalse(
+ WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
+ IncompleteExtensionWindowAreaStatus::class.java, 3))
+ }
+
+ /**
+ * Test that validator returns true if the [ExtensionWindowAreaPresentation] is valid
+ */
+ @Test
+ fun isExtensionWindowAreaPresentationValid_trueIfValid() {
+ assertTrue(
+ WindowAreaComponentValidator.isExtensionWindowAreaPresentationValid(
+ ValidExtensionWindowAreaPresentation::class.java, 3))
+ }
+
+ /**
+ * Test that validator returns false if the [ExtensionWindowAreaPresentation] is incomplete
+ */
+ @Test
+ fun isExtensionWindowAreaPresentationValid_falseIfIncomplete() {
+ assertFalse(
+ WindowAreaComponentValidator.isExtensionWindowAreaPresentationValid(
+ IncompleteExtensionWindowAreaPresentation::class.java, 3))
+ }
+
+ private class WindowAreaComponentFullImplementation : WindowAreaComponent {
+ 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 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")
+ }
+
+ 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")
+ }
+
+ override fun addRearDisplayPresentationStatusListener(
+ consumer: Consumer<ExtensionWindowAreaStatus>
+ ) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun removeRearDisplayPresentationStatusListener(
+ consumer: Consumer<ExtensionWindowAreaStatus>
+ ) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun startRearDisplayPresentationSession(
+ activity: Activity,
+ consumer: Consumer<Int>
+ ) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun endRearDisplayPresentationSession() {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun getRearDisplayPresentation(): ExtensionWindowAreaPresentation? {
+ throw NotImplementedError("Not implemented")
+ }
+ }
+
+ private class IncompleteWindowAreaComponentApiV2Implementation {
+ @Suppress("UNUSED_PARAMETER")
+ fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ fun removeRearDisplayStatusListener(consumer: Consumer<Int>) {
+ throw NotImplementedError("Not implemented")
+ }
+ }
+
+ private class ValidExtensionWindowAreaPresentation : ExtensionWindowAreaPresentation {
+ override fun getPresentationContext(): Context {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun setPresentationView(view: View) {
+ throw NotImplementedError("Not implemented")
+ }
+ }
+
+ private class IncompleteExtensionWindowAreaPresentation {
+ fun getPresentationContext(): Context {
+ throw NotImplementedError("Not implemented")
+ }
+ }
+
+ private class ValidExtensionWindowAreaStatus : ExtensionWindowAreaStatus {
+ override fun getWindowAreaStatus(): Int {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun getWindowAreaDisplayMetrics(): DisplayMetrics {
+ throw NotImplementedError("Not implemented")
+ }
+ }
+
+ private class IncompleteExtensionWindowAreaStatus {
+ fun getWindowAreaStatus(): 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 9d0aaba..9388ae3 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
@@ -20,9 +20,13 @@
import androidx.window.extensions.embedding.SplitAttributes as OEMSplitAttributes
import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
import android.app.Activity
+import android.os.Binder
+import android.os.IBinder
import androidx.window.WindowTestUtils
import androidx.window.core.ExtensionsUtil
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.SplitAttributes.SplitType
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_HINGE
import androidx.window.extensions.WindowExtensions
@@ -55,12 +59,13 @@
OEMSplitAttributes.Builder().build(),
)
val expectedSplitInfo = SplitInfo(
- ActivityStack(ArrayList(), isEmpty = true),
- ActivityStack(ArrayList(), isEmpty = true),
+ ActivityStack(ArrayList(), isEmpty = true, INVALID_ACTIVITY_STACK_TOKEN),
+ ActivityStack(ArrayList(), isEmpty = true, INVALID_ACTIVITY_STACK_TOKEN),
SplitAttributes.Builder()
.setSplitType(SplitType.SPLIT_TYPE_EQUAL)
.setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
- .build()
+ .build(),
+ INVALID_SPLIT_INFO_TOKEN,
)
assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
}
@@ -77,12 +82,13 @@
.build(),
)
val expectedSplitInfo = SplitInfo(
- ActivityStack(ArrayList(), isEmpty = true),
- ActivityStack(ArrayList(), isEmpty = true),
+ ActivityStack(ArrayList(), isEmpty = true, INVALID_ACTIVITY_STACK_TOKEN),
+ ActivityStack(ArrayList(), isEmpty = true, INVALID_ACTIVITY_STACK_TOKEN),
SplitAttributes.Builder()
.setSplitType(SplitType.SPLIT_TYPE_EXPAND)
.setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
- .build()
+ .build(),
+ INVALID_SPLIT_INFO_TOKEN,
)
assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
}
@@ -101,13 +107,14 @@
}
val expectedSplitInfo = SplitInfo(
- ActivityStack(ArrayList(), isEmpty = true),
- ActivityStack(ArrayList(), isEmpty = true),
+ ActivityStack(ArrayList(), isEmpty = true, INVALID_ACTIVITY_STACK_TOKEN),
+ ActivityStack(ArrayList(), isEmpty = true, INVALID_ACTIVITY_STACK_TOKEN),
SplitAttributes.Builder()
.setSplitType(SplitType.ratio(expectedSplitRatio))
// OEMSplitInfo with Vendor API level 1 doesn't provide layoutDirection.
.setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
- .build()
+ .build(),
+ INVALID_SPLIT_INFO_TOKEN,
)
assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
}
@@ -125,12 +132,39 @@
.build(),
)
val expectedSplitInfo = SplitInfo(
- ActivityStack(ArrayList(), isEmpty = true),
- ActivityStack(ArrayList(), isEmpty = true),
+ ActivityStack(ArrayList(), isEmpty = true, INVALID_ACTIVITY_STACK_TOKEN),
+ ActivityStack(ArrayList(), isEmpty = true, INVALID_ACTIVITY_STACK_TOKEN),
SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_HINGE)
.setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
- .build()
+ .build(),
+ INVALID_SPLIT_INFO_TOKEN,
+ )
+ assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+ }
+
+ @Test
+ fun testTranslateSplitInfoWithApiLevel3() {
+ WindowTestUtils.assumeAtLeastVendorApiLevel(WindowExtensions.VENDOR_API_LEVEL_3)
+ 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, testStackToken),
+ ActivityStack(ArrayList(), isEmpty = true, testStackToken),
+ SplitAttributes.Builder()
+ .setSplitType(SPLIT_TYPE_HINGE)
+ .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
+ .build(),
+ testSplitInfoToken,
)
assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
}
@@ -139,6 +173,7 @@
testPrimaryActivityStack: OEMActivityStack,
testSecondaryActivityStack: OEMActivityStack,
testSplitAttributes: OEMSplitAttributes,
+ testToken: IBinder = INVALID_SPLIT_INFO_TOKEN,
): OEMSplitInfo {
return mock<OEMSplitInfo>().apply {
whenever(primaryActivityStack).thenReturn(testPrimaryActivityStack)
@@ -146,16 +181,23 @@
if (ExtensionsUtil.safeVendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_2) {
whenever(splitAttributes).thenReturn(testSplitAttributes)
}
+ if (ExtensionsUtil.safeVendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_3) {
+ whenever(token).thenReturn(testToken)
+ }
}
}
private fun createTestOEMActivityStack(
testActivities: List<Activity>,
testIsEmpty: Boolean,
+ testToken: IBinder = INVALID_ACTIVITY_STACK_TOKEN,
): OEMActivityStack {
return mock<OEMActivityStack>().apply {
whenever(activities).thenReturn(testActivities)
whenever(isEmpty).thenReturn(testIsEmpty)
+ if (ExtensionsUtil.safeVendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_3) {
+ whenever(token).thenReturn(testToken)
+ }
}
}
-}
\ No newline at end of file
+}
diff --git a/window/window/src/main/java/androidx/window/SafeWindowExtensionsProvider.kt b/window/window/src/main/java/androidx/window/SafeWindowExtensionsProvider.kt
index 2aa82f4..c33b215 100644
--- a/window/window/src/main/java/androidx/window/SafeWindowExtensionsProvider.kt
+++ b/window/window/src/main/java/androidx/window/SafeWindowExtensionsProvider.kt
@@ -16,12 +16,26 @@
package androidx.window
+import androidx.window.extensions.WindowExtensions
+import androidx.window.extensions.WindowExtensionsProvider
import androidx.window.reflection.ReflectionUtils
import androidx.window.reflection.ReflectionUtils.doesReturn
import androidx.window.reflection.ReflectionUtils.isPublic
import androidx.window.reflection.WindowExtensionsConstants
internal class SafeWindowExtensionsProvider(private val loader: ClassLoader) {
+
+ val windowExtensions: WindowExtensions?
+ get() {
+ return try {
+ if (isWindowExtensionsPresent() && isWindowExtensionsValid()) {
+ WindowExtensionsProvider.getWindowExtensions()
+ } else null
+ } catch (e: Exception) {
+ null
+ }
+ }
+
internal val windowExtensionsClass: Class<*>
get() {
return loader.loadClass(WindowExtensionsConstants.WINDOW_EXTENSIONS_CLASS)
diff --git a/window/window/src/main/java/androidx/window/WindowProperties.kt b/window/window/src/main/java/androidx/window/WindowProperties.kt
index 156652f..1fc74f6 100644
--- a/window/window/src/main/java/androidx/window/WindowProperties.kt
+++ b/window/window/src/main/java/androidx/window/WindowProperties.kt
@@ -87,4 +87,39 @@
*/
const val PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED =
"android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
+
+ /**
+ * Application level
+ * [PackageManager][android.content.pm.PackageManager.Property] tag
+ * for an app to inform the system that the app can be opted-out from the compatibility
+ * treatment that avoids [android.app.Activity.setRequestedOrientation] loops. The loop
+ * can be trigerred when ignoreOrientationRequest display setting is
+ * enabled on the device (enables compatibility mode for fixed orientation,
+ * see [Enhanced letterboxing](https://developer.android.com/guide/practices/enhanced-letterboxing)
+ * for more details). or by the landscape natural orientation of the device.
+ *
+ *
+ * The system could ignore [android.app.Activity.setRequestedOrientation]
+ * call from an app if both of the following conditions are true:
+ * * Activity has requested orientation more than 2 times within 1-second timer
+ * * Activity is not letterboxed for fixed orientation
+ *
+ * Setting this property to `false` informs the system that the app must be
+ * opted-out from the compatibility treatment even if the device manufacturer has opted the app
+ * into the treatment.
+ *
+ * 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_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED"
+ * android:value="false" />
+ * </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"
}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/EmptyWindowAreaControllerImpl.kt b/window/window/src/main/java/androidx/window/area/EmptyWindowAreaControllerImpl.kt
index d56eccb..07fcfd5 100644
--- a/window/window/src/main/java/androidx/window/area/EmptyWindowAreaControllerImpl.kt
+++ b/window/window/src/main/java/androidx/window/area/EmptyWindowAreaControllerImpl.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,27 +17,36 @@
package androidx.window.area
import android.app.Activity
-import androidx.window.core.ExperimentalWindowApi
+import android.os.Binder
+import java.util.concurrent.Executor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
-import java.util.concurrent.Executor
/**
- * Empty Implementation for devices that do not
- * support the [WindowAreaController] functionality
+ * Empty Implementation for devices that do not support the [WindowAreaController] functionality
*/
-@ExperimentalWindowApi
internal class EmptyWindowAreaControllerImpl : WindowAreaController {
- override fun rearDisplayStatus(): Flow<WindowAreaStatus> {
- return flowOf(WindowAreaStatus.UNSUPPORTED)
- }
- override fun rearDisplayMode(
+ override val windowAreaInfos: Flow<List<WindowAreaInfo>>
+ get() = flowOf(listOf())
+
+ override fun transferActivityToWindowArea(
+ token: Binder,
activity: Activity,
executor: Executor,
windowAreaSessionCallback: WindowAreaSessionCallback
) {
- // TODO(b/269144982): Investigate not throwing an exception
- throw UnsupportedOperationException("Rear Display mode cannot be enabled currently")
+ windowAreaSessionCallback.onSessionEnded(
+ IllegalStateException("There are no WindowAreas"))
+ }
+
+ override fun presentContentOnWindowArea(
+ token: Binder,
+ activity: Activity,
+ executor: Executor,
+ windowAreaPresentationSessionCallback: WindowAreaPresentationSessionCallback
+ ) {
+ windowAreaPresentationSessionCallback.onSessionEnded(
+ IllegalStateException("There are no WindowAreas"))
}
}
diff --git a/window/window/src/main/java/androidx/window/area/RearDisplayPresentationSessionPresenterImpl.kt b/window/window/src/main/java/androidx/window/area/RearDisplayPresentationSessionPresenterImpl.kt
new file mode 100644
index 0000000..4c06141
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/RearDisplayPresentationSessionPresenterImpl.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.window.area
+
+import android.content.Context
+import android.view.View
+import androidx.window.extensions.area.ExtensionWindowAreaPresentation
+import androidx.window.extensions.area.WindowAreaComponent
+
+internal class RearDisplayPresentationSessionPresenterImpl(
+ private val windowAreaComponent: WindowAreaComponent,
+ private val presentation: ExtensionWindowAreaPresentation
+) : WindowAreaSessionPresenter {
+
+ override val context: Context = presentation.presentationContext
+
+ override fun setContentView(view: View) {
+ presentation.setPresentationView(view)
+ }
+
+ override fun close() {
+ windowAreaComponent.endRearDisplayPresentationSession()
+ }
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/RearDisplaySessionImpl.kt b/window/window/src/main/java/androidx/window/area/RearDisplaySessionImpl.kt
index ae7d3ca..5a4a9a3 100644
--- a/window/window/src/main/java/androidx/window/area/RearDisplaySessionImpl.kt
+++ b/window/window/src/main/java/androidx/window/area/RearDisplaySessionImpl.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,10 +16,8 @@
package androidx.window.area
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.extensions.area.WindowAreaComponent
-@ExperimentalWindowApi
internal class RearDisplaySessionImpl(
private val windowAreaComponent: WindowAreaComponent
) : WindowAreaSession {
diff --git a/window/window/src/main/java/androidx/window/area/SafeWindowAreaComponentProvider.kt b/window/window/src/main/java/androidx/window/area/SafeWindowAreaComponentProvider.kt
new file mode 100644
index 0000000..8eb9696
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/SafeWindowAreaComponentProvider.kt
@@ -0,0 +1,92 @@
+/*
+ * 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
+
+import androidx.window.SafeWindowExtensionsProvider
+import androidx.window.area.reflectionguard.WindowAreaComponentValidator.isExtensionWindowAreaPresentationValid
+import androidx.window.area.reflectionguard.WindowAreaComponentValidator.isExtensionWindowAreaStatusValid
+import androidx.window.area.reflectionguard.WindowAreaComponentValidator.isWindowAreaComponentValid
+import androidx.window.core.ExtensionsUtil
+import androidx.window.extensions.area.WindowAreaComponent
+import androidx.window.reflection.ReflectionUtils.doesReturn
+import androidx.window.reflection.ReflectionUtils.isPublic
+import androidx.window.reflection.ReflectionUtils.validateReflection
+import androidx.window.reflection.WindowExtensionsConstants
+
+/**
+ * Reflection Guard for [WindowAreaComponent].
+ * This will go through the [WindowAreaComponent]'s method by reflection and
+ * check each method's name and signature to see if the interface is what we required.
+ */
+internal class SafeWindowAreaComponentProvider(private val loader: ClassLoader) {
+
+ private val windowExtensions = SafeWindowExtensionsProvider(loader).windowExtensions
+
+ val windowAreaComponent: WindowAreaComponent?
+ get() {
+ return try {
+ if (
+ windowExtensions != null &&
+ isWindowAreaProviderValid(windowExtensions) &&
+ isWindowAreaComponentValid(
+ windowAreaComponentClass, ExtensionsUtil.safeVendorApiLevel
+ ) &&
+ isExtensionWindowAreaStatusValid(
+ extensionWindowAreaStatusClass, ExtensionsUtil.safeVendorApiLevel
+ ) &&
+ isValidExtensionWindowPresentation()
+ ) windowExtensions.windowAreaComponent else null
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ private fun isWindowAreaProviderValid(windowExtensions: Any): Boolean {
+ return validateReflection(
+ "WindowExtensions#getWindowAreaComponent is not valid"
+ ) {
+ val getWindowAreaComponentMethod =
+ windowExtensions::class.java.getMethod("getWindowAreaComponent")
+ getWindowAreaComponentMethod.isPublic &&
+ getWindowAreaComponentMethod.doesReturn(windowAreaComponentClass)
+ }
+ }
+
+ 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)
+ }
+
+ private val extensionWindowAreaStatusClass: Class<*>
+ get() {
+ return loader.loadClass(WindowExtensionsConstants.EXTENSION_WINDOW_AREA_STATUS_CLASS)
+ }
+
+ private val extensionWindowAreaPresentationClass: Class<*>
+ get() {
+ return loader.loadClass(
+ WindowExtensionsConstants.EXTENSION_WINDOW_AREA_PRESENTATION_CLASS
+ )
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaAdapter.kt b/window/window/src/main/java/androidx/window/area/WindowAreaAdapter.kt
index 65154449..65adc81 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaAdapter.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaAdapter.kt
@@ -16,21 +16,42 @@
package androidx.window.area
-import androidx.window.core.ExperimentalWindowApi
+import android.util.DisplayMetrics
+import androidx.core.view.WindowInsetsCompat
+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.core.Bounds
import androidx.window.extensions.area.WindowAreaComponent
+import androidx.window.extensions.area.WindowAreaComponent.STATUS_ACTIVE
+import androidx.window.extensions.area.WindowAreaComponent.STATUS_AVAILABLE
+import androidx.window.extensions.area.WindowAreaComponent.STATUS_UNAVAILABLE
+import androidx.window.extensions.area.WindowAreaComponent.STATUS_UNSUPPORTED
+import androidx.window.layout.WindowMetrics
/**
* Adapter object to assist in translating values received from [WindowAreaComponent]
* to developer friendly values in [WindowAreaController]
*/
-@ExperimentalWindowApi
internal object WindowAreaAdapter {
- internal fun translate(status: @WindowAreaComponent.WindowAreaStatus Int): WindowAreaStatus {
+ internal fun translate(displayMetrics: DisplayMetrics): WindowMetrics {
+ return WindowMetrics(
+ Bounds(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels),
+ WindowInsetsCompat.Builder().build()
+ )
+ }
+
+ internal fun translate(
+ status: @WindowAreaComponent.WindowAreaStatus Int
+ ): WindowAreaCapability.Status {
return when (status) {
- WindowAreaComponent.STATUS_AVAILABLE -> WindowAreaStatus.AVAILABLE
- WindowAreaComponent.STATUS_UNAVAILABLE -> WindowAreaStatus.UNAVAILABLE
- else -> WindowAreaStatus.UNSUPPORTED
+ STATUS_UNSUPPORTED -> WINDOW_AREA_STATUS_UNSUPPORTED
+ STATUS_UNAVAILABLE -> WINDOW_AREA_STATUS_UNAVAILABLE
+ STATUS_AVAILABLE -> WINDOW_AREA_STATUS_AVAILABLE
+ STATUS_ACTIVE -> WINDOW_AREA_STATUS_ACTIVE
+ else -> WINDOW_AREA_STATUS_UNSUPPORTED
}
}
}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaCapability.kt b/window/window/src/main/java/androidx/window/area/WindowAreaCapability.kt
new file mode 100644
index 0000000..3346bf2
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaCapability.kt
@@ -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.area
+
+import android.app.Activity
+
+/**
+ * Represents a capability for a [WindowAreaInfo].
+ */
+class WindowAreaCapability internal constructor(val operation: Operation, val status: Status) {
+ override fun toString(): String {
+ return "Operation: $operation: Status: $status"
+ }
+
+ /**
+ * Represents the status of availability for a specific [WindowAreaCapability]
+ */
+ class Status private constructor(private val description: String) {
+ override fun toString(): String {
+ return description
+ }
+
+ companion object {
+ /**
+ * Status indicating that the WindowArea feature is not a supported feature on the
+ * device.
+ */
+ @JvmField
+ val WINDOW_AREA_STATUS_UNSUPPORTED = Status("UNSUPPORTED")
+
+ /**
+ * Status indicating that the WindowArea feature is currently not available to be
+ * enabled. This could be because a different feature is active, or the current device
+ * configuration doesn't allow it.
+ */
+ @JvmField
+ val WINDOW_AREA_STATUS_UNAVAILABLE = Status("UNAVAILABLE")
+
+ /**
+ * Status indicating that the WindowArea feature is available to be enabled.
+ */
+ @JvmField
+ val WINDOW_AREA_STATUS_AVAILABLE = Status("AVAILABLE")
+
+ /**
+ * Status indicating that the WindowArea feature is currently active.
+ */
+ @JvmField
+ val WINDOW_AREA_STATUS_ACTIVE = Status("ACTIVE")
+ }
+ }
+
+ /**
+ * Represents an operation that a [WindowAreaInfo] may support.
+ */
+ class Operation private constructor(private val description: String) {
+ override fun toString(): String {
+ return description
+ }
+
+ companion object {
+
+ /**
+ * Operation that transfers an [Activity] into a [WindowAreaInfo]
+ */
+ @JvmField
+ val OPERATION_TRANSFER_ACTIVITY_TO_AREA = Operation("TRANSFER")
+
+ /**
+ * Operation that presents additional content into a [WindowAreaInfo]
+ */
+ @JvmField
+ val OPERATION_PRESENT_ON_AREA = Operation("PRESENT")
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is WindowAreaCapability &&
+ operation == other.operation &&
+ status == other.status
+ }
+
+ override fun hashCode(): Int {
+ var result = operation.hashCode()
+ result = 31 * result + status.hashCode()
+ return result
+ }
+}
\ No newline at end of file
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 cc6f9eb..951e671 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,55 +17,112 @@
package androidx.window.area
import android.app.Activity
+import android.os.Binder
import android.os.Build
import android.util.Log
import androidx.annotation.RestrictTo
+import androidx.window.area.WindowAreaInfo.Type.Companion.TYPE_REAR_FACING
import androidx.window.core.BuildConfig
-import androidx.window.core.ExperimentalWindowApi
+import androidx.window.core.ExtensionsUtil
import androidx.window.core.VerificationMode
-import androidx.window.extensions.WindowExtensionsProvider
-import androidx.window.extensions.area.WindowAreaComponent
import java.util.concurrent.Executor
import kotlinx.coroutines.flow.Flow
/**
- * An interface to provide information about available window areas on the device and an option
- * to use the rear display area of a foldable device, exclusively or concurrently with the internal
- * display.
- *
- * @hide
+ * An interface to provide the information and behavior around moving windows between
+ * displays or display areas on a device.
*
*/
-@ExperimentalWindowApi
interface WindowAreaController {
/**
- * Provides information about the current state of the window area of the rear display on the
- * device, if or when it is available. Rear Display mode can be invoked if the current status is
- * [WindowAreaStatus.AVAILABLE].
+ * [Flow] of the list of current [WindowAreaInfo]s that are currently available to be interacted
+ * with.
*/
- fun rearDisplayStatus(): Flow<WindowAreaStatus>
+ val windowAreaInfos: Flow<List<WindowAreaInfo>>
/**
- * Starts Rear Display Mode and moves the provided activity to the rear side of the device in
- * order to face the same direction as the primary device camera(s). When a rear display
- * mode is started, the system will turn on the rear display of the device to show the content
- * there, and can disable the internal display. The provided [Activity] is likely to get a
- * configuration change or being relaunched due to the difference in the internal and rear
- * display sizes on the device.
- * <p>Only the top visible application can request and use this mode. The system can dismiss the
- * mode if the user changes the device state.
- * <p>This method can only be called if the feature is supported on the device and is reported
- * as available in the current state through [rearDisplayStatus], otherwise it will
- * throw an [Exception].
+ * Starts a transfer session where the calling [Activity] is moved to the window area identified
+ * by the [token]. Updates on the session are provided through the [WindowAreaSessionCallback].
+ * Attempting to start a transfer session when the [WindowAreaInfo] does not return
+ * [WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE] will result in
+ * [WindowAreaSessionCallback.onSessionEnded] containing an [IllegalStateException]
+ *
+ * Only the top visible application can request to start a transfer session.
+ *
+ * The calling [Activity] will likely go through a configuration change since the window area
+ * it will be transferred to is usually different from the current area the [Activity] is in.
+ * The callback is retained during the lifetime of the session. If an [Activity] is captured in
+ * the callback and it does not handle the configuration change then it will be leaked. Consider
+ * using an [androidx.lifecycle.ViewModel] since that is meant to outlive the [Activity]
+ * lifecycle. If the [Activity] does override configuration changes, it is safe to have the
+ * [Activity] handle the WindowAreaSessionCallback. This guarantees that the calling [Activity]
+ * will continue to receive [WindowAreaSessionCallback.onSessionEnded] and keep a handle to the
+ * [WindowAreaSession] provided through [WindowAreaSessionCallback.onSessionStarted].
+ *
+ * The [windowAreaSessionCallback] provided will receive a call to
+ * [WindowAreaSessionCallback.onSessionStarted] after the [Activity] has been transferred to the
+ * window area. The transfer session will stay active until the session provided through
+ * [WindowAreaSessionCallback.onSessionStarted] is closed. Depending on the
+ * [WindowAreaInfo.Type] there may be other triggers that end the session, such as if a device
+ * state change makes the window area unavailable. One example of this is if the [Activity] is
+ * currently transferred to the [TYPE_REAR_FACING] window area of a foldable device, the session
+ * will be ended when the device is closed. When this occurs,
+ * [WindowAreaSessionCallback.onSessionEnded] is called.
+ *
+ * @param token [Binder] token identifying the window area to be transferred to.
+ * @param activity Base Activity making the call to [transferActivityToWindowArea].
+ * @param executor Executor used to provide updates to [windowAreaSessionCallback].
+ * @param windowAreaSessionCallback to be notified when the rear display session is started and
+ * ended.
+ *
+ * @see windowAreaInfos
*/
- fun rearDisplayMode(
+ fun transferActivityToWindowArea(
+ token: Binder,
activity: Activity,
executor: Executor,
+ // TODO(272064992) investigate how to make this safer from leaks
windowAreaSessionCallback: WindowAreaSessionCallback
)
+ /**
+ * Starts a presentation session on the [WindowAreaInfo] identified by the [token] and sends
+ * updates through the [WindowAreaPresentationSessionCallback].
+ *
+ * If a presentation session is attempted to be started without it being available,
+ * [WindowAreaPresentationSessionCallback.onSessionEnded] will be called immediately with an
+ * [IllegalStateException].
+ *
+ * Only the top visible application can request to start a presentation session.
+ *
+ * The presentation session will stay active until the presentation provided through
+ * [WindowAreaPresentationSessionCallback.onSessionStarted] is closed. The [WindowAreaInfo.Type]
+ * may provide different triggers to close the session such as if the calling application
+ * is no longer in the foreground, or there is a device state change that makes the window area
+ * unavailable to be presented on. One example scenario is if a [TYPE_REAR_FACING] window area
+ * is being presented to on a foldable device that is open and has 2 screens. If the device is
+ * closed and the internal display is turned off, the session would be ended and
+ * [WindowAreaPresentationSessionCallback.onSessionEnded] is called to notify that the session
+ * has been ended. The session may end prematurely if the device gets to a critical thermal
+ * level, or if power saver mode is enabled.
+ *
+ * @param token [Binder] token to identify which [WindowAreaInfo] is to be presented on
+ * @param activity An [Activity] that will present content on the Rear Display.
+ * @param executor Executor used to provide updates to [windowAreaPresentationSessionCallback].
+ * @param windowAreaPresentationSessionCallback to be notified of updates to the lifecycle of
+ * the currently enabled rear display presentation.
+ * @see windowAreaInfos
+ */
+ fun presentContentOnWindowArea(
+ token: Binder,
+ activity: Activity,
+ executor: Executor,
+ windowAreaPresentationSessionCallback: WindowAreaPresentationSessionCallback
+ )
+
public companion object {
+
private val TAG = WindowAreaController::class.simpleName
private var decorator: WindowAreaControllerDecorator = EmptyDecorator
@@ -76,24 +133,23 @@
@JvmName("getOrCreate")
@JvmStatic
fun getOrCreate(): WindowAreaController {
- var windowAreaComponentExtensions: WindowAreaComponent?
- try {
- // TODO(b/267972002): Introduce reflection guard for WindowAreaComponent
- windowAreaComponentExtensions = WindowExtensionsProvider
- .getWindowExtensions()
- .windowAreaComponent
+ val windowAreaComponentExtensions = try {
+ this::class.java.classLoader?.let {
+ SafeWindowAreaComponentProvider(it).windowAreaComponent
+ }
} catch (t: Throwable) {
- if (BuildConfig.verificationMode == VerificationMode.STRICT) {
+ if (BuildConfig.verificationMode == VerificationMode.LOG) {
Log.d(TAG, "Failed to load WindowExtensions")
}
- windowAreaComponentExtensions = null
+ null
}
val controller =
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N ||
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q ||
windowAreaComponentExtensions == null) {
EmptyWindowAreaControllerImpl()
} else {
- WindowAreaControllerImpl(windowAreaComponentExtensions)
+ WindowAreaControllerImpl(
+ windowAreaComponentExtensions, ExtensionsUtil.safeVendorApiLevel)
}
return decorator.decorate(controller)
}
@@ -116,7 +172,6 @@
* Decorator that allows us to provide different functionality
* in our window-testing artifact.
*/
-@ExperimentalWindowApi
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
interface WindowAreaControllerDecorator {
/**
@@ -126,7 +181,6 @@
public fun decorate(controller: WindowAreaController): WindowAreaController
}
-@ExperimentalWindowApi
private object EmptyDecorator : WindowAreaControllerDecorator {
override fun decorate(controller: WindowAreaController): WindowAreaController {
return controller
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
index af9a398..b7b1afc 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,21 +17,31 @@
package androidx.window.area
import android.app.Activity
+import android.os.Binder
import android.os.Build
+import android.util.DisplayMetrics
import android.util.Log
import androidx.annotation.RequiresApi
+import androidx.core.view.WindowInsetsCompat
+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_UNSUPPORTED
+import androidx.window.core.Bounds
import androidx.window.core.BuildConfig
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.core.VerificationMode
+import androidx.window.extensions.area.ExtensionWindowAreaStatus
import androidx.window.extensions.area.WindowAreaComponent
import androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_ACTIVE
import androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_INACTIVE
+import androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_CONTENT_INVISIBLE
+import androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_CONTENT_VISIBLE
+import androidx.window.extensions.area.WindowAreaComponent.WindowAreaSessionState
import androidx.window.extensions.core.util.function.Consumer
+import androidx.window.layout.WindowMetrics
import java.util.concurrent.Executor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.distinctUntilChanged
/**
* Implementation of WindowAreaController for devices
@@ -42,52 +52,217 @@
* [Build.VERSION_CODES.S] as that's the min level of support for
* this functionality.
*/
-@ExperimentalWindowApi
-@RequiresApi(Build.VERSION_CODES.N)
+@RequiresApi(Build.VERSION_CODES.Q)
internal class WindowAreaControllerImpl(
- private val windowAreaComponent: WindowAreaComponent
+ private val windowAreaComponent: WindowAreaComponent,
+ private val vendorApiLevel: Int
) : WindowAreaController {
- private var currentStatus: WindowAreaStatus? = null
+ private lateinit var rearDisplaySessionConsumer: Consumer<Int>
+ private var currentRearDisplayModeStatus: WindowAreaCapability.Status =
+ WINDOW_AREA_STATUS_UNSUPPORTED
+ private var currentRearDisplayPresentationStatus: WindowAreaCapability.Status =
+ WINDOW_AREA_STATUS_UNSUPPORTED
- override fun rearDisplayStatus(): Flow<WindowAreaStatus> {
- return callbackFlow {
- val listener = Consumer<@WindowAreaComponent.WindowAreaStatus Int> { status ->
- currentStatus = WindowAreaAdapter.translate(status)
- channel.trySend(currentStatus ?: WindowAreaStatus.UNSUPPORTED)
+ private val currentWindowAreaInfoMap = HashMap<String, WindowAreaInfo>()
+
+ override val windowAreaInfos: Flow<List<WindowAreaInfo>>
+ get() {
+ return callbackFlow {
+ val rearDisplayListener = Consumer<Int> { status ->
+ updateRearDisplayAvailability(status)
+ channel.trySend(currentWindowAreaInfoMap.values.toList())
+ }
+ val rearDisplayPresentationListener =
+ Consumer<ExtensionWindowAreaStatus> { extensionWindowAreaStatus ->
+ updateRearDisplayPresentationAvailability(extensionWindowAreaStatus)
+ channel.trySend(currentWindowAreaInfoMap.values.toList())
+ }
+
+ windowAreaComponent.addRearDisplayStatusListener(rearDisplayListener)
+ if (vendorApiLevel > 2) {
+ windowAreaComponent.addRearDisplayPresentationStatusListener(
+ rearDisplayPresentationListener
+ )
+ }
+
+ awaitClose {
+ windowAreaComponent.removeRearDisplayStatusListener(rearDisplayListener)
+ if (vendorApiLevel > 2) {
+ windowAreaComponent.removeRearDisplayPresentationStatusListener(
+ rearDisplayPresentationListener
+ )
+ }
+ }
}
- windowAreaComponent.addRearDisplayStatusListener(listener)
- awaitClose {
- windowAreaComponent.removeRearDisplayStatusListener(listener)
- }
- }.distinctUntilChanged()
+ }
+
+ private fun updateRearDisplayAvailability(
+ status: @WindowAreaComponent.WindowAreaStatus Int
+ ) {
+ currentRearDisplayModeStatus = WindowAreaAdapter.translate(status)
+ updateRearDisplayWindowArea(
+ WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA,
+ currentRearDisplayModeStatus,
+ createEmptyWindowMetrics() /* metrics */,
+ )
}
- override fun rearDisplayMode(
+ private fun updateRearDisplayPresentationAvailability(
+ extensionWindowAreaStatus: ExtensionWindowAreaStatus
+ ) {
+ currentRearDisplayPresentationStatus =
+ WindowAreaAdapter.translate(extensionWindowAreaStatus.windowAreaStatus)
+ val windowMetrics = WindowAreaAdapter.translate(
+ displayMetrics = extensionWindowAreaStatus.windowAreaDisplayMetrics
+ )
+
+ updateRearDisplayWindowArea(
+ WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA,
+ currentRearDisplayPresentationStatus,
+ windowMetrics,
+ )
+ }
+
+ private fun updateRearDisplayWindowArea(
+ operation: WindowAreaCapability.Operation,
+ status: WindowAreaCapability.Status,
+ metrics: WindowMetrics,
+ ) {
+ var rearDisplayAreaInfo: WindowAreaInfo? =
+ currentWindowAreaInfoMap[REAR_DISPLAY_BINDER_DESCRIPTOR]
+ if (status == WINDOW_AREA_STATUS_UNSUPPORTED) {
+ rearDisplayAreaInfo?.let { info ->
+ if (shouldRemoveWindowAreaInfo(info)) {
+ currentWindowAreaInfoMap.remove(REAR_DISPLAY_BINDER_DESCRIPTOR)
+ } else {
+ val capability = WindowAreaCapability(operation, status)
+ info.capabilityMap[operation] = capability
+ }
+ }
+ } else {
+ if (rearDisplayAreaInfo == null) {
+ rearDisplayAreaInfo = WindowAreaInfo(
+ metrics = metrics,
+ type = WindowAreaInfo.Type.TYPE_REAR_FACING,
+ // TODO(b/273807238): Update extensions to send the binder token and type
+ token = Binder(REAR_DISPLAY_BINDER_DESCRIPTOR),
+ windowAreaComponent = windowAreaComponent
+ )
+ }
+ val capability = WindowAreaCapability(operation, status)
+ rearDisplayAreaInfo.capabilityMap[operation] = capability
+ currentWindowAreaInfoMap[REAR_DISPLAY_BINDER_DESCRIPTOR] = rearDisplayAreaInfo
+ }
+ rearDisplayAreaInfo?.metrics = metrics
+ }
+
+ /**
+ * Determines if a [WindowAreaInfo] should be removed from [windowAreaInfos] if all
+ * [WindowAreaCapability] are currently [WINDOW_AREA_STATUS_UNSUPPORTED]
+ */
+ private fun shouldRemoveWindowAreaInfo(windowAreaInfo: WindowAreaInfo): Boolean {
+ for (capability: WindowAreaCapability in windowAreaInfo.capabilityMap.values) {
+ if (capability.status != WINDOW_AREA_STATUS_UNSUPPORTED) {
+ return false
+ }
+ }
+ return true
+ }
+
+ override fun transferActivityToWindowArea(
+ token: Binder,
+ activity: Activity,
+ executor: Executor,
+ windowAreaSessionCallback: WindowAreaSessionCallback
+ ) {
+ if (token.interfaceDescriptor == REAR_DISPLAY_BINDER_DESCRIPTOR) {
+ startRearDisplayMode(activity, executor, windowAreaSessionCallback)
+ }
+ }
+
+ override fun presentContentOnWindowArea(
+ token: Binder,
+ activity: Activity,
+ executor: Executor,
+ windowAreaPresentationSessionCallback: WindowAreaPresentationSessionCallback
+ ) {
+ if (token.interfaceDescriptor == REAR_DISPLAY_BINDER_DESCRIPTOR) {
+ startRearDisplayPresentationMode(
+ activity,
+ executor,
+ windowAreaPresentationSessionCallback
+ )
+ }
+ }
+
+ private fun startRearDisplayMode(
activity: Activity,
executor: Executor,
windowAreaSessionCallback: WindowAreaSessionCallback
) {
- // If we already have a status value that is not [WindowAreaStatus.AVAILABLE]
- // we should throw an exception quick to indicate they tried to enable
- // RearDisplay mode when it was not available.
- if (currentStatus != null && currentStatus != WindowAreaStatus.AVAILABLE) {
- throw UnsupportedOperationException("Rear Display mode cannot be enabled currently")
+ // If the capability is currently active, provide an error pointing the developer on how to
+ // get access to the current session
+ if (currentRearDisplayModeStatus == WINDOW_AREA_STATUS_ACTIVE) {
+ windowAreaSessionCallback.onSessionEnded(
+ IllegalStateException(
+ "The WindowArea feature is currently active, WindowAreaInfo#getActiveSession" +
+ "can be used to get an instance of the current active session"
+ )
+ )
+ return
}
- val rearDisplaySessionConsumer =
+
+ // If we already have an availability value that is not
+ // [Availability.WINDOW_AREA_CAPABILITY_AVAILABLE] we should end the session and pass an
+ // exception to indicate they tried to enable rear display mode when it was not available.
+ if (currentRearDisplayModeStatus != WINDOW_AREA_STATUS_AVAILABLE) {
+ windowAreaSessionCallback.onSessionEnded(
+ IllegalStateException(
+ "The WindowArea feature is currently not available to be entered"
+ )
+ )
+ return
+ }
+
+ rearDisplaySessionConsumer =
RearDisplaySessionConsumer(executor, windowAreaSessionCallback, windowAreaComponent)
windowAreaComponent.startRearDisplaySession(activity, rearDisplaySessionConsumer)
}
+ private fun startRearDisplayPresentationMode(
+ activity: Activity,
+ executor: Executor,
+ windowAreaPresentationSessionCallback: WindowAreaPresentationSessionCallback
+ ) {
+ if (currentRearDisplayPresentationStatus != WINDOW_AREA_STATUS_AVAILABLE) {
+ windowAreaPresentationSessionCallback.onSessionEnded(
+ IllegalStateException(
+ "The WindowArea feature is currently not available to be entered"
+ )
+ )
+ return
+ }
+
+ windowAreaComponent.startRearDisplayPresentationSession(
+ activity,
+ RearDisplayPresentationSessionConsumer(
+ executor,
+ windowAreaPresentationSessionCallback,
+ windowAreaComponent
+ )
+ )
+ }
+
internal class RearDisplaySessionConsumer(
private val executor: Executor,
private val appCallback: WindowAreaSessionCallback,
private val extensionsComponent: WindowAreaComponent
- ) : Consumer<@WindowAreaComponent.WindowAreaSessionState Int> {
+ ) : Consumer<Int> {
private var session: WindowAreaSession? = null
- override fun accept(t: @WindowAreaComponent.WindowAreaSessionState Int) {
+ override fun accept(t: Int) {
when (t) {
SESSION_STATE_ACTIVE -> onSessionStarted()
SESSION_STATE_INACTIVE -> onSessionFinished()
@@ -107,11 +282,54 @@
private fun onSessionFinished() {
session = null
- executor.execute { appCallback.onSessionEnded() }
+ executor.execute { appCallback.onSessionEnded(null) }
+ }
+ }
+
+ internal class RearDisplayPresentationSessionConsumer(
+ private val executor: Executor,
+ private val windowAreaPresentationSessionCallback: WindowAreaPresentationSessionCallback,
+ private val windowAreaComponent: WindowAreaComponent
+ ) : Consumer<@WindowAreaSessionState Int> {
+ override fun accept(t: @WindowAreaSessionState Int) {
+ executor.execute {
+ when (t) {
+ // Presentation should never be null if the session is active
+ SESSION_STATE_ACTIVE -> windowAreaPresentationSessionCallback.onSessionStarted(
+ RearDisplayPresentationSessionPresenterImpl(
+ windowAreaComponent,
+ windowAreaComponent.rearDisplayPresentation!!
+ )
+ )
+
+ SESSION_STATE_CONTENT_VISIBLE ->
+ windowAreaPresentationSessionCallback.onContainerVisibilityChanged(true)
+
+ SESSION_STATE_CONTENT_INVISIBLE ->
+ windowAreaPresentationSessionCallback.onContainerVisibilityChanged(false)
+
+ SESSION_STATE_INACTIVE ->
+ windowAreaPresentationSessionCallback.onSessionEnded(null)
+
+ else -> {
+ Log.e(TAG, "Invalid session state value received: $t")
+ }
+ }
+ }
}
}
internal companion object {
private val TAG = WindowAreaControllerImpl::class.simpleName
+
+ private const val REAR_DISPLAY_BINDER_DESCRIPTOR = "WINDOW_AREA_REAR_DISPLAY"
+
+ internal fun createEmptyWindowMetrics(): WindowMetrics {
+ val displayMetrics = DisplayMetrics()
+ return WindowMetrics(
+ Bounds(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels),
+ WindowInsetsCompat.Builder().build()
+ )
+ }
}
}
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaInfo.kt b/window/window/src/main/java/androidx/window/area/WindowAreaInfo.kt
new file mode 100644
index 0000000..e38bdaa
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaInfo.kt
@@ -0,0 +1,132 @@
+/*
+ * 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
+
+import android.os.Binder
+import androidx.window.area.WindowAreaCapability.Operation.Companion.OPERATION_PRESENT_ON_AREA
+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.extensions.area.WindowAreaComponent
+import androidx.window.layout.WindowMetrics
+
+/**
+ * The current state of a window area. The [WindowAreaInfo] can represent a part of or an entire
+ * display in the system. These values can be used to modify the UI to show/hide controls and
+ * determine when features can be enabled.
+ */
+class WindowAreaInfo internal constructor(
+
+ /**
+ * The [WindowMetrics] that represent the size of the area. Used to determine if the behavior
+ * desired fits the size of the window area available.
+ */
+ var metrics: WindowMetrics,
+
+ /**
+ * The [Type] of this window area
+ */
+ val type: Type,
+
+ /**
+ * [Binder] token to identify the specific WindowArea
+ */
+ val token: Binder,
+
+ private val windowAreaComponent: WindowAreaComponent
+) {
+
+ internal val capabilityMap = HashMap<WindowAreaCapability.Operation, WindowAreaCapability>()
+
+ /**
+ * Returns the [WindowAreaCapability] corresponding to the [operation] provided. If this
+ * [WindowAreaCapability] does not exist for this [WindowAreaInfo], null is returned.
+ */
+ fun getCapability(operation: WindowAreaCapability.Operation): WindowAreaCapability? {
+ return capabilityMap[operation]
+ }
+
+ /**
+ * Returns the current active [WindowAreaSession] is one is currently active for the provided
+ * [operation]
+ *
+ * @throws IllegalStateException if there is no active session for the provided [operation]
+ */
+ fun getActiveSession(operation: WindowAreaCapability.Operation): WindowAreaSession? {
+ if (getCapability(operation)?.status != WINDOW_AREA_STATUS_ACTIVE) {
+ throw IllegalStateException("No session is currently active")
+ }
+
+ if (type == Type.TYPE_REAR_FACING) {
+ // TODO(b/273807246) We should cache instead of always creating a new session
+ return createRearFacingSession(operation)
+ }
+ return null
+ }
+
+ private fun createRearFacingSession(
+ operation: WindowAreaCapability.Operation
+ ): WindowAreaSession {
+ return when (operation) {
+ OPERATION_TRANSFER_ACTIVITY_TO_AREA -> RearDisplaySessionImpl(windowAreaComponent)
+ OPERATION_PRESENT_ON_AREA ->
+ RearDisplayPresentationSessionPresenterImpl(
+ windowAreaComponent,
+ windowAreaComponent.rearDisplayPresentation!!
+ )
+ else -> {
+ throw IllegalArgumentException("Invalid operation provided")
+ }
+ }
+ }
+
+ /**
+ * Represents a type of [WindowAreaInfo]
+ */
+ class Type private constructor(private val description: String) {
+ override fun toString(): String {
+ return description
+ }
+
+ companion object {
+ /**
+ * Type of window area that is facing the same direction as the rear camera(s) on the
+ * device.
+ */
+ @JvmField
+ val TYPE_REAR_FACING = Type("REAR FACING")
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is WindowAreaInfo &&
+ metrics == other.metrics &&
+ type == other.type &&
+ capabilityMap.entries == other.capabilityMap.entries
+ }
+
+ override fun hashCode(): Int {
+ var result = metrics.hashCode()
+ result = 31 * result + type.hashCode()
+ result = 31 * result + capabilityMap.entries.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "WindowAreaInfo{ Metrics: $metrics, type: $type, " +
+ "Capabilities: ${capabilityMap.entries} }"
+ }
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaPresentationSessionCallback.kt b/window/window/src/main/java/androidx/window/area/WindowAreaPresentationSessionCallback.kt
new file mode 100644
index 0000000..2d4b8ce
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaPresentationSessionCallback.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.window.area
+
+import android.content.Context
+import android.view.View
+
+/**
+ * A callback to notify about the lifecycle of a window area presentation session.
+ *
+ * @see WindowAreaController.presentContentOnWindowArea
+ */
+interface WindowAreaPresentationSessionCallback {
+
+ /**
+ * Notifies about a start of a presentation session. Provides a reference to
+ * [WindowAreaSessionPresenter] to allow an application to customize a presentation when the
+ * session starts. The [Context] provided from the [WindowAreaSessionPresenter] should be used
+ * to inflate or make any UI decisions around the presentation [View] that should be shown in
+ * that area.
+ */
+ fun onSessionStarted(session: WindowAreaSessionPresenter)
+
+ /**
+ * Notifies about an end of a presentation session. The presentation and any app-provided
+ * content in the window area is removed.
+ *
+ * @param t [Throwable] to provide information on if the session was ended due to an error.
+ * This will only occur if a session is attempted to be enabled when it is not available, but
+ * can be expanded to alert for more errors in the future.
+ */
+ fun onSessionEnded(t: Throwable?)
+
+ /**
+ * Notifies about changes in visibility of a container that can hold the app content to show
+ * in the window area. Notification of the container being visible is guaranteed to occur after
+ * [onSessionStarted] has been called. The container being no longer visible is guaranteed to
+ * occur before [onSessionEnded].
+ *
+ * If content was never presented, then this method will never be called.
+ */
+ fun onContainerVisibilityChanged(isVisible: Boolean)
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaSession.kt b/window/window/src/main/java/androidx/window/area/WindowAreaSession.kt
index e3e4bff..41ca43ea 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaSession.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaSession.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,17 +16,15 @@
package androidx.window.area
-import androidx.window.core.ExperimentalWindowApi
-
/**
- * Session interface to represent a long-standing
- * WindowArea mode or feature that provides a handle
- * to close the session.
+ * Session interface to represent an active window area feature.
*
- * @hide
- *
+ * @see WindowAreaSessionCallback.onSessionStarted
*/
-@ExperimentalWindowApi
interface WindowAreaSession {
+
+ /**
+ * Closes the active session, no-op if the session is not currently active.
+ */
fun close()
}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaSessionCallback.kt b/window/window/src/main/java/androidx/window/area/WindowAreaSessionCallback.kt
index 7527d53..b76e175 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaSessionCallback.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaSessionCallback.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,20 +16,25 @@
package androidx.window.area
-import androidx.window.core.ExperimentalWindowApi
-
/**
- * Callback to update the client on the WindowArea Session being
+ * Callback to update the client on the WindowArea Session being
* started and ended.
* TODO(b/207720511) Move to window-java module when Kotlin API Finalized
- *
- * @hide
- *
*/
-@ExperimentalWindowApi
interface WindowAreaSessionCallback {
+ /**
+ * Notifies about a start of a session. Provides a reference to the current [WindowAreaSession]
+ * the application the ability to close the session through [WindowAreaSession.close].
+ */
fun onSessionStarted(session: WindowAreaSession)
- fun onSessionEnded()
+ /**
+ * Notifies about an end of a [WindowAreaSession].
+ *
+ * @param t [Throwable] to provide information on if the session was ended due to an error.
+ * This will only occur if a session is attempted to be enabled when it is not available, but
+ * can be expanded to alert for more errors in the future.
+ */
+ fun onSessionEnded(t: Throwable?)
}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaSessionPresenter.kt b/window/window/src/main/java/androidx/window/area/WindowAreaSessionPresenter.kt
new file mode 100644
index 0000000..bc8bfc8
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaSessionPresenter.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.window.area
+
+import android.content.Context
+import android.view.View
+
+/**
+ * A container that allows getting access to and showing content on a window area. The container is
+ * provided from [WindowAreaPresentationSessionCallback] when a requested session becomes active.
+ * The presentation can be automatically dismissed by the system when the user leaves the primary
+ * application window, or can be closed by calling [WindowAreaSessionPresenter.close].
+ * @see WindowAreaController.presentContentOnWindowArea
+ */
+interface WindowAreaSessionPresenter : WindowAreaSession {
+ /**
+ * Returns the [Context] associated with the window area.
+ */
+ val context: Context
+
+ /**
+ * 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.
+ */
+ fun setContentView(view: View)
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaStatus.kt b/window/window/src/main/java/androidx/window/area/WindowAreaStatus.kt
deleted file mode 100644
index 732da7d..0000000
--- a/window/window/src/main/java/androidx/window/area/WindowAreaStatus.kt
+++ /dev/null
@@ -1,56 +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
-
-import androidx.window.core.ExperimentalWindowApi
-
-/**
- * Represents a window area status.
- *
- * @hide
- *
- */
-@ExperimentalWindowApi
-class WindowAreaStatus private constructor(private val mDescription: String) {
-
- override fun toString(): String {
- return mDescription
- }
-
- companion object {
- /**
- * Status representing that the WindowArea feature is not a supported
- * feature on the device.
- */
- @JvmField
- val UNSUPPORTED = WindowAreaStatus("UNSUPPORTED")
-
- /**
- * Status representing that the WindowArea feature is currently not available
- * to be enabled. This could be due to another process has enabled it, or that the
- * current device configuration doesn't allow it.
- */
- @JvmField
- val UNAVAILABLE = WindowAreaStatus("UNAVAILABLE")
-
- /**
- * Status representing that the WindowArea feature is available to be enabled.
- */
- @JvmField
- val AVAILABLE = WindowAreaStatus("AVAILABLE")
- }
-}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaPresentationRequirements.java b/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaPresentationRequirements.java
new file mode 100644
index 0000000..9153250
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaPresentationRequirements.java
@@ -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.area.reflectionguard;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.area.ExtensionWindowAreaPresentation;
+
+/**
+ * API requirements for [ExtensionWindowAreaPresentation]
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface ExtensionWindowAreaPresentationRequirements {
+ /** @see ExtensionWindowAreaPresentation#getPresentationContext */
+ @NonNull
+ Context getPresentationContext();
+
+ /** @see ExtensionWindowAreaPresentation#setPresentationView */
+ void setPresentationView(@NonNull View view);
+}
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaStatusRequirements.java b/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaStatusRequirements.java
new file mode 100644
index 0000000..14ba999
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaStatusRequirements.java
@@ -0,0 +1,36 @@
+/*
+ * 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.util.DisplayMetrics;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.area.ExtensionWindowAreaStatus;
+
+/**
+ * API requirements for [ExtensionWindowAreaStatus]
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface ExtensionWindowAreaStatusRequirements {
+ /** @see ExtensionWindowAreaStatus#getWindowAreaStatus */
+ int getWindowAreaStatus();
+
+ /** @see ExtensionWindowAreaStatus#getWindowAreaDisplayMetrics */
+ @NonNull
+ DisplayMetrics getWindowAreaDisplayMetrics();
+}
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
new file mode 100644
index 0000000..0ab78c0
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi2Requirements.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.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
new file mode 100644
index 0000000..aad8216
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi3Requirements.java
@@ -0,0 +1,58 @@
+/*
+ * 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.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.area.ExtensionWindowAreaPresentation;
+import androidx.window.extensions.area.ExtensionWindowAreaStatus;
+import androidx.window.extensions.area.WindowAreaComponent;
+import androidx.window.extensions.core.util.function.Consumer;
+
+
+/**
+ * This file defines the Vendor API Level 3 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 WindowAreaComponentApi3Requirements extends WindowAreaComponentApi2Requirements {
+
+ /** @see WindowAreaComponent#addRearDisplayPresentationStatusListener */
+ void addRearDisplayPresentationStatusListener(
+ @NonNull Consumer<ExtensionWindowAreaStatus> consumer);
+
+ /** @see WindowAreaComponent#removeRearDisplayPresentationStatusListener */
+ void removeRearDisplayPresentationStatusListener(
+ @NonNull Consumer<ExtensionWindowAreaStatus> consumer);
+
+ /** @see WindowAreaComponent#startRearDisplayPresentationSession */
+ void startRearDisplayPresentationSession(@NonNull Activity activity,
+ @NonNull Consumer<Integer> consumer);
+
+ /** @see WindowAreaComponent#endRearDisplayPresentationSession */
+ void endRearDisplayPresentationSession();
+
+ /** @see WindowAreaComponent#getRearDisplayPresentation */
+ @Nullable
+ ExtensionWindowAreaPresentation getRearDisplayPresentation();
+}
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
new file mode 100644
index 0000000..d48d2ab
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt
@@ -0,0 +1,63 @@
+/*
+ * 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 androidx.window.extensions.area.ExtensionWindowAreaPresentation
+import androidx.window.extensions.area.WindowAreaComponent
+import androidx.window.reflection.ReflectionUtils.validateImplementation
+
+/**
+ * Utility class to validate [WindowAreaComponent] implementation.
+ */
+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
+ )
+ }
+ }
+
+ internal fun isExtensionWindowAreaStatusValid(
+ extensionWindowAreaStatus: Class<*>,
+ apiLevel: Int
+ ): Boolean {
+ return when {
+ apiLevel <= 1 -> false
+ else -> validateImplementation(
+ extensionWindowAreaStatus, ExtensionWindowAreaStatusRequirements::class.java
+ )
+ }
+ }
+
+ internal fun isExtensionWindowAreaPresentationValid(
+ extensionWindowAreaPresentation: Class<*>,
+ apiLevel: Int
+ ): Boolean {
+ return when {
+ apiLevel <= 2 -> false
+ else -> validateImplementation(
+ extensionWindowAreaPresentation, ExtensionWindowAreaPresentation::class.java
+ )
+ }
+ }
+}
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 219cb1f..8adffae 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt
@@ -17,7 +17,10 @@
package androidx.window.embedding
import android.app.Activity
+import android.app.ActivityOptions
import android.content.Context
+import android.os.IBinder
+import androidx.window.core.ExperimentalWindowApi
/** The controller that allows checking the current [Activity] embedding status. */
class ActivityEmbeddingController internal constructor(private val backend: EmbeddingBackend) {
@@ -31,6 +34,68 @@
fun isActivityEmbedded(activity: Activity): Boolean =
backend.isActivityEmbedded(activity)
+ /**
+ * Returns the [ActivityStack] that this [activity] is part of when it is being organized in the
+ * embedding container and associated with a [SplitInfo]. Returns `null` if there is no such
+ * [ActivityStack].
+ *
+ * @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].
+ *
+ * @param options The [android.app.ActivityOptions] to be updated.
+ * @param token The token of the [ActivityStack] to be set.
+ */
+ internal fun setLaunchingActivityStack(
+ options: ActivityOptions,
+ token: IBinder
+ ): ActivityOptions {
+ return backend.setLaunchingActivityStack(options, token)
+ }
+
+ /**
+ * Finishes a set of [activityStacks][ActivityStack] from the lowest to the highest z-order
+ * regardless of the order of [ActivityStack] set.
+ *
+ * If the remaining [ActivityStack] from a split participates in other splits with other
+ * `activityStacks`, they might be showing instead. 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 associated `activityStacks` of a [ActivityStack] are finished, the [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 calling
+ * [isFinishingActivityStacksSupported]. 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 this device doesn't support this API and
+ * [isFinishingActivityStacksSupported] returns `false`.
+ * @sample androidx.window.samples.embedding.expandPrimaryContainer
+ */
+ @ExperimentalWindowApi
+ fun finishActivityStacks(activityStacks: Set<ActivityStack>) =
+ backend.finishActivityStacks(activityStacks)
+
+ /**
+ * Checks whether [finishActivityStacks] is supported.
+ *
+ * @return `true` if [finishActivityStacks] is supported on the device, `false` otherwise.
+ */
+ @ExperimentalWindowApi
+ fun isFinishingActivityStacksSupported(): Boolean =
+ backend.isFinishActivityStacksSupported()
+
companion object {
/**
* Obtains an instance of [ActivityEmbeddingController].
@@ -43,4 +108,4 @@
return ActivityEmbeddingController(backend)
}
}
-}
\ No newline at end of file
+}
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..190ffa3
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptions.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.
+ */
+@file:JvmName("ActivityEmbeddingOptions")
+
+package androidx.window.embedding
+
+import android.app.Activity
+import android.app.ActivityOptions
+import android.content.Context
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.core.ExtensionsUtil
+import androidx.window.extensions.WindowExtensions
+
+/**
+ * Sets the launching [ActivityStack] to the given [android.app.ActivityOptions].
+ *
+ * If the device doesn't support setting launching, [UnsupportedOperationException] will be thrown.
+ * @see isSetLaunchingActivityStackSupported
+ *
+ * @param context The [android.content.Context] that is going to be used for launching
+ * activity with this [android.app.ActivityOptions], 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 this device doesn't support this API.
+ */
+@ExperimentalWindowApi
+fun ActivityOptions.setLaunchingActivityStack(
+ context: Context,
+ activityStack: ActivityStack
+): ActivityOptions = let {
+ if (!isSetLaunchingActivityStackSupported()) {
+ throw UnsupportedOperationException("#setLaunchingActivityStack is not " +
+ "supported on the device.")
+ } else {
+ ActivityEmbeddingController.getInstance(context)
+ .setLaunchingActivityStack(this, activityStack.token)
+ }
+}
+
+/**
+ * Sets the launching [ActivityStack] to the [android.app.ActivityOptions] by the
+ * given [activity]. That is, the [ActivityStack] of the given [activity] is the
+ * [ActivityStack] used for launching.
+ *
+ * If the device doesn't support setting launching or no available [ActivityStack]
+ * can be found from the given [activity], [UnsupportedOperationException] will be thrown.
+ * @see isSetLaunchingActivityStackSupported
+ *
+ * @param activity The existing [android.app.Activity] on the target [ActivityStack].
+ * @throws UnsupportedOperationException if this device doesn't support this API or no
+ * available [ActivityStack] can be found.
+ */
+@ExperimentalWindowApi
+fun ActivityOptions.setLaunchingActivityStack(activity: Activity): ActivityOptions {
+ val activityStack =
+ ActivityEmbeddingController.getInstance(activity).getActivityStack(activity)
+ return if (activityStack != null) {
+ setLaunchingActivityStack(activity, activityStack)
+ } else {
+ throw UnsupportedOperationException("No available ActivityStack found. " +
+ "The given activity may not be embedded.")
+ }
+}
+
+/**
+ * Return `true` if the [setLaunchingActivityStack] APIs is supported and can be used
+ * to set the launching [ActivityStack]. Otherwise, return `false`.
+ */
+@ExperimentalWindowApi
+fun ActivityOptions.isSetLaunchingActivityStackSupported(): Boolean {
+ return ExtensionsUtil.safeVendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_3
+}
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 59b0839..ba9f8a9 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
@@ -16,6 +16,7 @@
package androidx.window.embedding
import android.app.Activity
+import android.os.IBinder
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
@@ -40,7 +41,11 @@
* process(es), [activitiesInProcess] will return an empty list, but this method will return
* `false`.
*/
- val isEmpty: Boolean
+ val isEmpty: Boolean,
+ /**
+ * A token uniquely identifying this `ActivityStack`.
+ */
+ internal val token: IBinder,
) {
/**
@@ -56,6 +61,7 @@
if (activitiesInProcess != other.activitiesInProcess) return false
if (isEmpty != other.isEmpty) return false
+ if (token != other.token) return false
return true
}
@@ -63,6 +69,7 @@
override fun hashCode(): Int {
var result = activitiesInProcess.hashCode()
result = 31 * result + isEmpty.hashCode()
+ result = 31 * result + token.hashCode()
return result
}
@@ -70,5 +77,6 @@
"ActivityStack{" +
"activitiesInProcess=$activitiesInProcess" +
", isEmpty=$isEmpty" +
+ ", token=$token" +
"}"
}
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 6dd98b7..f66230e 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
@@ -32,9 +32,9 @@
import android.app.Activity
import android.content.Context
import android.content.Intent
+import android.os.Binder
import android.util.LayoutDirection
import android.view.WindowMetrics
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.core.ExtensionsUtil
import androidx.window.core.PredicateAdapter
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.BOTTOM_TO_TOP
@@ -56,7 +56,6 @@
import androidx.window.extensions.embedding.SplitPairRule.FINISH_NEVER
import androidx.window.layout.WindowMetricsCalculator
import androidx.window.layout.adapter.extensions.ExtensionsWindowLayoutInfoAdapter
-import kotlin.Pair
/**
* Adapter class that translates data classes between Extension and Jetpack interfaces.
@@ -82,13 +81,16 @@
SplitInfo(
ActivityStack(
primaryActivityStack.activities,
- primaryActivityStack.isEmpty
+ primaryActivityStack.isEmpty,
+ primaryActivityStack.token,
),
ActivityStack(
secondaryActivityStack.activities,
- secondaryActivityStack.isEmpty
+ secondaryActivityStack.isEmpty,
+ secondaryActivityStack.token,
),
- translate(splitInfo.splitAttributes)
+ translate(splitInfo.splitAttributes),
+ splitInfo.token,
)
}
}
@@ -117,14 +119,12 @@
)
.build()
- @OptIn(ExperimentalWindowApi::class)
fun translateSplitAttributesCalculator(
calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
): Function<OEMSplitAttributesCalculatorParams, OEMSplitAttributes> = Function { oemParams ->
translateSplitAttributes(calculator.invoke(translate(oemParams)))
}
- @OptIn(ExperimentalWindowApi::class)
@SuppressLint("NewApi")
fun translate(
params: OEMSplitAttributesCalculatorParams
@@ -322,18 +322,21 @@
val primaryActivityStack = splitInfo.primaryActivityStack
val primaryFragment = ActivityStack(
primaryActivityStack.activities,
- primaryActivityStack.isEmpty
+ primaryActivityStack.isEmpty,
+ INVALID_ACTIVITY_STACK_TOKEN,
)
val secondaryActivityStack = splitInfo.secondaryActivityStack
val secondaryFragment = ActivityStack(
secondaryActivityStack.activities,
- secondaryActivityStack.isEmpty
+ secondaryActivityStack.isEmpty,
+ INVALID_ACTIVITY_STACK_TOKEN,
)
return SplitInfo(
primaryFragment,
secondaryFragment,
- translate(splitInfo.splitAttributes)
+ translate(splitInfo.splitAttributes),
+ INVALID_SPLIT_INFO_TOKEN,
)
}
}
@@ -501,12 +504,28 @@
ActivityStack(
splitInfo.primaryActivityStack.activities,
splitInfo.primaryActivityStack.isEmpty,
+ INVALID_ACTIVITY_STACK_TOKEN,
),
ActivityStack(
splitInfo.secondaryActivityStack.activities,
splitInfo.secondaryActivityStack.isEmpty,
+ INVALID_ACTIVITY_STACK_TOKEN,
),
getSplitAttributesCompat(splitInfo),
+ INVALID_SPLIT_INFO_TOKEN,
)
}
+
+ internal companion object {
+ /**
+ * The default token of [SplitInfo], which provides compatibility for device prior to
+ * [WindowExtensions.VENDOR_API_LEVEL_3]
+ */
+ val INVALID_SPLIT_INFO_TOKEN = Binder()
+ /**
+ * The default token of [ActivityStack], which provides compatibility for device prior to
+ * [WindowExtensions.VENDOR_API_LEVEL_3]
+ */
+ val INVALID_ACTIVITY_STACK_TOKEN = Binder()
+ }
}
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 02b8a67..c8825c0 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
@@ -17,10 +17,11 @@
package androidx.window.embedding
import android.app.Activity
+import android.app.ActivityOptions
import android.content.Context
+import android.os.IBinder
import androidx.annotation.RestrictTo
import androidx.core.util.Consumer
-import androidx.window.core.ExperimentalWindowApi
import java.util.concurrent.Executor
/**
@@ -50,7 +51,6 @@
fun isActivityEmbedded(activity: Activity): Boolean
- @ExperimentalWindowApi
fun setSplitAttributesCalculator(
calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
)
@@ -59,6 +59,20 @@
fun isSplitAttributesCalculatorSupported(): Boolean
+ fun getActivityStack(activity: Activity): ActivityStack?
+
+ fun setLaunchingActivityStack(options: ActivityOptions, token: IBinder): ActivityOptions
+
+ fun finishActivityStacks(activityStacks: Set<ActivityStack>)
+
+ fun isFinishActivityStacksSupported(): Boolean
+
+ fun invalidateTopVisibleSplitAttributes()
+
+ fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes)
+
+ fun areSplitAttributesUpdatesSupported(): Boolean
+
companion object {
private var decorator: (EmbeddingBackend) -> EmbeddingBackend =
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 6a1248c..c1c872a 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
@@ -18,16 +18,18 @@
import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
import android.app.Activity
+import android.app.ActivityOptions
import android.content.Context
+import android.os.IBinder
import android.util.Log
import androidx.window.core.BuildConfig
import androidx.window.core.ConsumerAdapter
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.core.ExtensionsUtil
import androidx.window.core.VerificationMode
import androidx.window.embedding.EmbeddingInterfaceCompat.EmbeddingCallbackInterface
import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_AVAILABLE
import androidx.window.extensions.WindowExtensions.VENDOR_API_LEVEL_2
+import androidx.window.extensions.WindowExtensions.VENDOR_API_LEVEL_3
import androidx.window.extensions.WindowExtensionsProvider
import androidx.window.extensions.core.util.function.Consumer
import androidx.window.extensions.embedding.ActivityEmbeddingComponent
@@ -90,7 +92,6 @@
return embeddingExtension.isActivityEmbedded(activity)
}
- @ExperimentalWindowApi
override fun setSplitAttributesCalculator(
calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
) {
@@ -114,6 +115,50 @@
override fun isSplitAttributesCalculatorSupported(): Boolean =
ExtensionsUtil.safeVendorApiLevel >= VENDOR_API_LEVEL_2
+ override fun finishActivityStacks(activityStacks: Set<ActivityStack>) {
+ if (!isFinishActivityStacksSupported()) {
+ throw UnsupportedOperationException("#finishActivityStacks is not " +
+ "supported on the device.")
+ }
+ val stackTokens = activityStacks.mapTo(mutableSetOf()) { it.token }
+ embeddingExtension.finishActivityStacks(stackTokens)
+ }
+
+ override fun isFinishActivityStacksSupported(): Boolean =
+ ExtensionsUtil.safeVendorApiLevel >= VENDOR_API_LEVEL_3
+
+ override fun invalidateTopVisibleSplitAttributes() {
+ if (!areSplitAttributesUpdatesSupported()) {
+ throw UnsupportedOperationException("#invalidateTopVisibleSplitAttributes is not " +
+ "supported on the device.")
+ }
+ embeddingExtension.invalidateTopVisibleSplitAttributes()
+ }
+
+ override fun updateSplitAttributes(
+ splitInfo: SplitInfo,
+ splitAttributes: SplitAttributes
+ ) {
+ if (!areSplitAttributesUpdatesSupported()) {
+ throw UnsupportedOperationException("#updateSplitAttributes is not supported on the " +
+ "device.")
+ }
+ embeddingExtension.updateSplitAttributes(
+ splitInfo.token,
+ adapter.translateSplitAttributes(splitAttributes)
+ )
+ }
+
+ override fun areSplitAttributesUpdatesSupported(): Boolean =
+ ExtensionsUtil.safeVendorApiLevel >= VENDOR_API_LEVEL_3
+
+ override fun setLaunchingActivityStack(
+ options: ActivityOptions,
+ token: IBinder
+ ): ActivityOptions {
+ return embeddingExtension.setLaunchingActivityStack(options, token)
+ }
+
companion object {
const val DEBUG = true
private const val TAG = "EmbeddingCompat"
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 c9830a5..26d8846 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
@@ -17,7 +17,8 @@
package androidx.window.embedding
import android.app.Activity
-import androidx.window.core.ExperimentalWindowApi
+import android.app.ActivityOptions
+import android.os.IBinder
import androidx.window.extensions.embedding.ActivityEmbeddingComponent
/**
@@ -36,7 +37,6 @@
fun isActivityEmbedded(activity: Activity): Boolean
- @ExperimentalWindowApi
fun setSplitAttributesCalculator(
calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
)
@@ -44,4 +44,16 @@
fun clearSplitAttributesCalculator()
fun isSplitAttributesCalculatorSupported(): Boolean
+
+ fun setLaunchingActivityStack(options: ActivityOptions, token: IBinder): ActivityOptions
+
+ fun finishActivityStacks(activityStacks: Set<ActivityStack>)
+
+ fun isFinishActivityStacksSupported(): Boolean
+
+ fun invalidateTopVisibleSplitAttributes()
+
+ fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes)
+
+ fun areSplitAttributesUpdatesSupported(): Boolean
}
\ No newline at end of file
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 d8371b7..9a931db 100644
--- a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
@@ -17,9 +17,11 @@
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.util.Log
import androidx.annotation.DoNotInline
import androidx.annotation.GuardedBy
@@ -30,7 +32,6 @@
import androidx.window.WindowProperties
import androidx.window.core.BuildConfig
import androidx.window.core.ConsumerAdapter
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.core.ExtensionsUtil
import androidx.window.core.PredicateAdapter
import androidx.window.core.VerificationMode
@@ -335,7 +336,6 @@
return embeddingExtension?.isActivityEmbedded(activity) ?: false
}
- @ExperimentalWindowApi
override fun setSplitAttributesCalculator(
calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
) {
@@ -353,6 +353,49 @@
override fun isSplitAttributesCalculatorSupported(): Boolean =
embeddingExtension?.isSplitAttributesCalculatorSupported() ?: false
+ 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
+ }
+ }
+
+ override fun setLaunchingActivityStack(
+ options: ActivityOptions,
+ token: IBinder
+ ): ActivityOptions = embeddingExtension?.setLaunchingActivityStack(options, token) ?: options
+
+ override fun finishActivityStacks(activityStacks: Set<ActivityStack>) {
+ embeddingExtension?.finishActivityStacks(activityStacks)
+ }
+
+ override fun isFinishActivityStacksSupported(): Boolean =
+ embeddingExtension?.isFinishActivityStacksSupported() ?: false
+
+ override fun invalidateTopVisibleSplitAttributes() {
+ embeddingExtension?.invalidateTopVisibleSplitAttributes()
+ }
+
+ override fun updateSplitAttributes(
+ splitInfo: SplitInfo,
+ splitAttributes: SplitAttributes
+ ) {
+ embeddingExtension?.updateSplitAttributes(splitInfo, splitAttributes)
+ }
+
+ override fun areSplitAttributesUpdatesSupported(): Boolean =
+ embeddingExtension?.areSplitAttributesUpdatesSupported() ?: false
@RequiresApi(31)
private object Api31Impl {
@DoNotInline
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculatorParams.kt b/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculatorParams.kt
index 8da62c6..40453be 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculatorParams.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculatorParams.kt
@@ -18,7 +18,6 @@
import android.content.res.Configuration
import androidx.annotation.RestrictTo
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.layout.WindowLayoutInfo
import androidx.window.layout.WindowMetrics
@@ -27,7 +26,6 @@
* [SplitController.setSplitAttributesCalculator] and references the corresponding [SplitRule] by
* [splitRuleTag] if [SplitPairRule.tag] is specified.
*/
-@ExperimentalWindowApi
class SplitAttributesCalculatorParams @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor(
/** The parent container's [WindowMetrics] */
val parentWindowMetrics: WindowMetrics,
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 41f46b8..660b15b 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
@@ -155,9 +155,8 @@
* example, a foldable device with multiple screens can choose to collapse
* splits when apps run on the device's small display, but enable splits
* when apps run on the device's large display. In cases like this,
- * [splitSupportStatus] always returns [SplitSupportStatus.SPLIT_AVAILABLE], and if the
- * split is collapsed, activities are launched on top, following the non-activity
- * embedding model.
+ * [splitSupportStatus] always returns [SplitSupportStatus.SPLIT_AVAILABLE], and if the split is
+ * collapsed, activities are launched on top, following the non-activity embedding model.
*
* Also the [androidx.window.WindowProperties.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED]
* must be enabled in AndroidManifest within <application> in order to get the correct
@@ -213,7 +212,6 @@
* @throws UnsupportedOperationException if [isSplitAttributesCalculatorSupported] reports
* `false`
*/
- @ExperimentalWindowApi
fun setSplitAttributesCalculator(
calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
) {
@@ -227,19 +225,85 @@
* @throws UnsupportedOperationException if [isSplitAttributesCalculatorSupported] reports
* `false`
*/
- @ExperimentalWindowApi
fun clearSplitAttributesCalculator() {
embeddingBackend.clearSplitAttributesCalculator()
}
/** Returns whether [setSplitAttributesCalculator] is supported or not. */
- @ExperimentalWindowApi
fun isSplitAttributesCalculatorSupported(): Boolean =
embeddingBackend.isSplitAttributesCalculatorSupported()
/**
+ * 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 [SplitController.addSplitListener].
+ *
+ * The call will be ignored if there is no visible split.
+ *
+ * @throws UnsupportedOperationException if the device doesn't support this API.
+ */
+ @ExperimentalWindowApi
+ fun invalidateTopVisibleSplitAttributes() =
+ embeddingBackend.invalidateTopVisibleSplitAttributes()
+
+ /**
+ * Checks whether [invalidateTopVisibleSplitAttributes] is supported on the device.
+ *
+ * Invoking these APIs if the feature is not supported would trigger an
+ * [UnsupportedOperationException].
+ * @return `true` if the runtime APIs to update [SplitAttributes] are supported and can be
+ * called safely, `false` otherwise.
+ */
+ @ExperimentalWindowApi
+ fun isInvalidatingTopVisibleSplitAttributesSupported(): Boolean =
+ embeddingBackend.areSplitAttributesUpdatesSupported()
+
+ /**
+ * 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 most of the time otherwise.
+ *
+ * The provided split attributes will be used instead of the associated
+ * [SplitRule.defaultSplitAttributes].
+ *
+ * **Note** that the split attributes may be updated if split attributes calculator callback is
+ * registered and invoked. If [setSplitAttributesCalculator] is used, the callback will still be
+ * applied to each [SplitInfo] when there's either:
+ * - 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.
+ *
+ * @param splitInfo the split pair to update
+ * @param splitAttributes the [SplitAttributes] to be applied
+ * @throws UnsupportedOperationException if this device doesn't support this API
+ */
+ @ExperimentalWindowApi
+ fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes) =
+ embeddingBackend.updateSplitAttributes(splitInfo, splitAttributes)
+
+ /**
+ * Checks whether [updateSplitAttributes] is supported on the device.
+ *
+ * Invoking these APIs if the feature is not supported would trigger an
+ * [UnsupportedOperationException].
+ * @return `true` if the runtime APIs to update [SplitAttributes] are supported and can be
+ * called safely, `false` otherwise.
+ */
+ @ExperimentalWindowApi
+ fun isUpdatingSplitAttributesSupported(): Boolean =
+ embeddingBackend.areSplitAttributesUpdatesSupported()
+
+ /**
* A class to determine if activity splits with Activity Embedding are currently available.
- * "Depending on the split property declaration, device software version or user preferences
+ * Depending on the split property declaration, device software version or user preferences
* the feature might not be available.
*/
class SplitSupportStatus private constructor(private val rawValue: Int) {
@@ -291,4 +355,4 @@
return SplitController(backend)
}
}
-}
+}
\ No newline at end of file
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 f366ca7..81adda5 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
@@ -17,6 +17,7 @@
package androidx.window.embedding
import android.app.Activity
+import android.os.IBinder
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
@@ -31,7 +32,11 @@
*/
val secondaryActivityStack: ActivityStack,
/** The [SplitAttributes] of this split pair. */
- val splitAttributes: SplitAttributes
+ val splitAttributes: SplitAttributes,
+ /**
+ * A token uniquely identifying this `SplitInfo`.
+ */
+ internal val token: IBinder,
) {
/**
* Whether the [primaryActivityStack] or the [secondaryActivityStack] in this [SplitInfo]
@@ -49,6 +54,7 @@
if (primaryActivityStack != other.primaryActivityStack) return false
if (secondaryActivityStack != other.secondaryActivityStack) return false
if (splitAttributes != other.splitAttributes) return false
+ if (token != other.token) return false
return true
}
@@ -57,6 +63,7 @@
var result = primaryActivityStack.hashCode()
result = 31 * result + secondaryActivityStack.hashCode()
result = 31 * result + splitAttributes.hashCode()
+ result = 31 * result + token.hashCode()
return result
}
@@ -66,6 +73,7 @@
append("primaryActivityStack=$primaryActivityStack, ")
append("secondaryActivityStack=$secondaryActivityStack, ")
append("splitAttributes=$splitAttributes, ")
+ append("token=$token")
append("}")
}
}
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 c989e32..765876e 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
@@ -22,14 +22,13 @@
import android.view.WindowMetrics
import androidx.annotation.DoNotInline
import androidx.annotation.IntRange
-import androidx.annotation.OptIn
import androidx.annotation.RequiresApi
-import androidx.core.os.BuildCompat
import androidx.core.util.Preconditions
import androidx.window.embedding.EmbeddingAspectRatio.Companion.ALWAYS_ALLOW
import androidx.window.embedding.EmbeddingAspectRatio.Companion.ratio
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
import androidx.window.embedding.SplitRule.FinishBehavior.Companion.ADJACENT
import kotlin.math.min
@@ -231,15 +230,16 @@
* Verifies if the provided parent bounds satisfy the dimensions and aspect ratio requirements
* to apply the rule.
*/
- // TODO(b/265089843) remove after Build.VERSION_CODES.U released.
- @OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
internal fun checkParentMetrics(context: Context, parentMetrics: WindowMetrics): Boolean {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
return false
}
val bounds = Api30Impl.getBounds(parentMetrics)
- // TODO(b/265089843) replace with Build.VERSION.SDK_INT >= Build.VERSION_CODES.U
- val density = context.resources.displayMetrics.density
+ val density = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
+ context.resources.displayMetrics.density
+ } else {
+ Api34Impl.getDensity(parentMetrics, context)
+ }
return checkParentBounds(density, bounds)
}
@@ -288,6 +288,19 @@
}
}
+ @RequiresApi(34)
+ internal object Api34Impl {
+ @DoNotInline
+ fun getDensity(windowMetrics: WindowMetrics, context: Context): Float {
+ // TODO(b/265089843) remove the try catch after U is finalized.
+ return try {
+ windowMetrics.density
+ } catch (e: NoSuchMethodError) {
+ context.resources.displayMetrics.density
+ }
+ }
+ }
+
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SplitRule) return false
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 326486e..ed8b7ee 100644
--- a/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
+++ b/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
@@ -80,4 +80,16 @@
internal fun Method.doesReturn(clazz: Class<*>): Boolean {
return returnType.equals(clazz)
}
-}
\ No newline at end of file
+
+ internal fun validateImplementation(
+ implementation: Class<*>,
+ requirements: Class<*>,
+ ): Boolean {
+ return requirements.methods.all {
+ validateReflection("${implementation.name}#${it.name} is not valid") {
+ val implementedMethod = implementation.getMethod(it.name, *it.parameterTypes)
+ implementedMethod.isPublic && implementedMethod.doesReturn(it.returnType)
+ }
+ }
+ }
+}
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 341e019..b86fdc1 100644
--- a/window/window/src/main/java/androidx/window/reflection/WindowExtensionsConstants.kt
+++ b/window/window/src/main/java/androidx/window/reflection/WindowExtensionsConstants.kt
@@ -53,6 +53,27 @@
"$WINDOW_EXTENSIONS_PACKAGE_NAME.layout.WindowLayoutComponent"
/**
+ * Constant name for class [androidx.window.extensions.area.WindowAreaComponent]
+ * used for reflection
+ */
+ internal const val WINDOW_AREA_COMPONENT_CLASS =
+ "$WINDOW_EXTENSIONS_PACKAGE_NAME.area.WindowAreaComponent"
+
+ /**
+ * Constant name for class [androidx.window.extensions.area.ExtensionWindowAreaStatus]
+ * used for reflection
+ */
+ internal const val EXTENSION_WINDOW_AREA_STATUS_CLASS =
+ "$WINDOW_EXTENSIONS_PACKAGE_NAME.area.ExtensionWindowAreaStatus"
+
+ /**
+ * Constant name for class [androidx.window.extensions.area.ExtensionWindowAreaPresentation]
+ * used for reflection
+ */
+ internal const val EXTENSION_WINDOW_AREA_PRESENTATION_CLASS =
+ "$WINDOW_EXTENSIONS_PACKAGE_NAME.area.ExtensionWindowAreaPresentation"
+
+ /**
* Constant name for class [androidx.window.extensions.embedding.ActivityEmbeddingComponent]
* used for reflection
*/
diff --git a/window/window/src/test/java/androidx/window/area/WindowAreaAdapterUnitTest.kt b/window/window/src/test/java/androidx/window/area/WindowAreaAdapterUnitTest.kt
deleted file mode 100644
index 89a9808..0000000
--- a/window/window/src/test/java/androidx/window/area/WindowAreaAdapterUnitTest.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.window.area
-
-import androidx.window.core.ExperimentalWindowApi
-import androidx.window.extensions.area.WindowAreaComponent
-import org.junit.Test
-
-/**
- * Unit tests for [WindowAreaAdapter] that run on the JVM.
- */
-@OptIn(ExperimentalWindowApi::class)
-class WindowAreaAdapterUnitTest {
-
- @Test
- fun testWindowAreaStatusTranslateValueAvailable() {
- val expected = WindowAreaStatus.AVAILABLE
- val translateValue = WindowAreaAdapter.translate(WindowAreaComponent.STATUS_AVAILABLE)
- assert(expected == translateValue)
- }
-
- @Test
- fun testWindowAreaStatusTranslateValueUnavailable() {
- val expected = WindowAreaStatus.UNAVAILABLE
- val translateValue = WindowAreaAdapter.translate(WindowAreaComponent.STATUS_UNAVAILABLE)
- assert(expected == translateValue)
- }
-
- @Test
- fun testWindowAreaStatusTranslateValueUnsupported() {
- val expected = WindowAreaStatus.UNSUPPORTED
- val translateValue = WindowAreaAdapter.translate(WindowAreaComponent.STATUS_UNSUPPORTED)
- assert(expected == translateValue)
- }
-}
\ No newline at end of file
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 b13af0c..1f84586 100644
--- a/window/window/src/test/java/androidx/window/embedding/ActivityStackTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/ActivityStackTest.kt
@@ -17,7 +17,9 @@
package androidx.window.embedding
import android.app.Activity
+import android.os.Binder
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mockito.kotlin.mock
@@ -27,7 +29,7 @@
@Test
fun testContainsActivity() {
val activity = mock<Activity>()
- val stack = ActivityStack(listOf(activity), isEmpty = false)
+ val stack = ActivityStack(listOf(activity), isEmpty = false, Binder())
assertTrue(activity in stack)
}
@@ -35,10 +37,17 @@
@Test
fun testEqualsImpliesHashCode() {
val activity = mock<Activity>()
- val first = ActivityStack(listOf(activity), isEmpty = false)
- val second = ActivityStack(listOf(activity), isEmpty = false)
+ val token = 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 = Binder()
+ val third = ActivityStack(emptyList(), isEmpty = true, anotherToken)
+
+ assertNotEquals(first, third)
+ assertNotEquals(first.hashCode(), third.hashCode())
}
}
\ No newline at end of file
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 3ad6e58..e297829 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitControllerTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitControllerTest.kt
@@ -45,9 +45,10 @@
@Test
fun test_splitInfoListComesFromBackend() = testScope.runTest {
val expected = listOf(SplitInfo(
- ActivityStack(emptyList(), true),
- ActivityStack(emptyList(), true),
- SplitAttributes()
+ ActivityStack(emptyList(), true, mock()),
+ ActivityStack(emptyList(), true, mock()),
+ SplitAttributes(),
+ mock()
))
doAnswer { invocationOnMock ->
@Suppress("UNCHECKED_CAST")
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 07c0855..780bcf9 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt
@@ -17,6 +17,9 @@
package androidx.window.embedding
import android.app.Activity
+import android.os.Binder
+import android.os.IBinder
+import androidx.window.embedding.EmbeddingAdapter.Companion.INVALID_ACTIVITY_STACK_TOKEN
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -30,7 +33,8 @@
val firstStack = createTestActivityStack(listOf(activity))
val secondStack = createTestActivityStack(emptyList())
val attributes = SplitAttributes()
- val info = SplitInfo(firstStack, secondStack, attributes)
+ val token = Binder()
+ val info = SplitInfo(firstStack, secondStack, attributes, token)
assertTrue(info.contains(activity))
}
@@ -41,7 +45,8 @@
val firstStack = createTestActivityStack(emptyList())
val secondStack = createTestActivityStack(listOf(activity))
val attributes = SplitAttributes()
- val info = SplitInfo(firstStack, secondStack, attributes)
+ val token = Binder()
+ val info = SplitInfo(firstStack, secondStack, attributes, token)
assertTrue(info.contains(activity))
}
@@ -52,8 +57,9 @@
val firstStack = createTestActivityStack(emptyList())
val secondStack = createTestActivityStack(listOf(activity))
val attributes = SplitAttributes()
- val firstInfo = SplitInfo(firstStack, secondStack, attributes)
- val secondInfo = SplitInfo(firstStack, secondStack, attributes)
+ val token = Binder()
+ val firstInfo = SplitInfo(firstStack, secondStack, attributes, token)
+ val secondInfo = SplitInfo(firstStack, secondStack, attributes, token)
assertEquals(firstInfo, secondInfo)
assertEquals(firstInfo.hashCode(), secondInfo.hashCode())
@@ -62,5 +68,6 @@
private fun createTestActivityStack(
activitiesInProcess: List<Activity>,
isEmpty: Boolean = false,
- ): ActivityStack = ActivityStack(activitiesInProcess, isEmpty)
+ token: IBinder = INVALID_ACTIVITY_STACK_TOKEN,
+ ): ActivityStack = ActivityStack(activitiesInProcess, isEmpty, token)
}
\ No newline at end of file
diff --git a/window/window/src/test/resources/robolectric.properties b/window/window/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..69fde47
--- /dev/null
+++ b/window/window/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# robolectric properties
+# Temporary until we update Robolectric to support API level 34.
+sdk=33
diff --git a/work/work-benchmark/build.gradle b/work/work-benchmark/build.gradle
index d4a2c31..cd0c8a6 100644
--- a/work/work-benchmark/build.gradle
+++ b/work/work-benchmark/build.gradle
@@ -38,7 +38,7 @@
}
androidx {
- name = "Android WorkManager Benchmarks"
+ name = "WorkManager Benchmarks"
publish = Publish.NONE
inceptionYear = "2019"
description = "Android WorkManager Benchmark Library"
diff --git a/work/work-datatransfer/build.gradle b/work/work-datatransfer/build.gradle
new file mode 100644
index 0000000..77f47f7
--- /dev/null
+++ b/work/work-datatransfer/build.gradle
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+android {
+ namespace "androidx.work.datatransfer"
+}
+
+dependencies {
+ api(project(":work:work-runtime-ktx"))
+ api(libs.kotlinStdlib)
+}
+
+androidx {
+ name = "WorkManager Data Transfer"
+ publish = Publish.SNAPSHOT_ONLY
+ inceptionYear = "2023"
+ description = "Android WorkManager Data Transfer library"
+}
diff --git a/work/work-gcm/build.gradle b/work/work-gcm/build.gradle
index 562422b0..98a4351 100644
--- a/work/work-gcm/build.gradle
+++ b/work/work-gcm/build.gradle
@@ -47,7 +47,7 @@
}
androidx {
- name = "Android WorkManager GCMNetworkManager Support"
+ name = "WorkManager GCM Integration"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2019"
description = "Android WorkManager GCMNetworkManager Support"
diff --git a/work/work-inspection/build.gradle b/work/work-inspection/build.gradle
index 5385c55..5cfd833 100644
--- a/work/work-inspection/build.gradle
+++ b/work/work-inspection/build.gradle
@@ -42,7 +42,7 @@
}
androidx {
- name = "Android WorkManager Inspector"
+ name = "WorkManager Inspector"
type = LibraryType.IDE_PLUGIN
inceptionYear = "2020"
description = "The implementation of WorkManager Inspector."
diff --git a/work/work-lint/build.gradle b/work/work-lint/build.gradle
index 083e406..fb59083 100644
--- a/work/work-lint/build.gradle
+++ b/work/work-lint/build.gradle
@@ -32,7 +32,7 @@
}
androidx {
- name = "Android WorkManager Runtime Lint Checks"
+ name = "WorkManager Runtime Lint Checks"
type = LibraryType.LINT
inceptionYear = "2019"
description = "Android WorkManager Runtime Lint Checks"
diff --git a/work/work-multiprocess/build.gradle b/work/work-multiprocess/build.gradle
index 7b7697d..94c4112 100644
--- a/work/work-multiprocess/build.gradle
+++ b/work/work-multiprocess/build.gradle
@@ -49,7 +49,7 @@
}
androidx {
- name = "Android WorkManager Multiprocess Implementation"
+ name = "WorkManager Multiprocess"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2020"
description = "Android WorkManager runtime library"
diff --git a/work/work-runtime-ktx/build.gradle b/work/work-runtime-ktx/build.gradle
index f45007c..34051b4 100644
--- a/work/work-runtime-ktx/build.gradle
+++ b/work/work-runtime-ktx/build.gradle
@@ -40,7 +40,7 @@
}
androidx {
- name = "Android WorkManager Kotlin Extensions"
+ name = "WorkManager Kotlin Extensions"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Android WorkManager Kotlin Extensions"
diff --git a/work/work-runtime/build.gradle b/work/work-runtime/build.gradle
index 5e8d67b..ff562a1 100644
--- a/work/work-runtime/build.gradle
+++ b/work/work-runtime/build.gradle
@@ -96,7 +96,7 @@
}
androidx {
- name = "Android WorkManager Runtime"
+ name = "WorkManager Runtime"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Android WorkManager runtime library"
diff --git a/work/work-rxjava2/build.gradle b/work/work-rxjava2/build.gradle
index e8e0e7e..9779ab9 100644
--- a/work/work-rxjava2/build.gradle
+++ b/work/work-rxjava2/build.gradle
@@ -32,7 +32,7 @@
}
androidx {
- name = "Android WorkManager RxJava2 Support"
+ name = "WorkManager RxJava2"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Android WorkManager RxJava2 interoperatibility library"
diff --git a/work/work-rxjava3/build.gradle b/work/work-rxjava3/build.gradle
index a725099..c6e82b9 100644
--- a/work/work-rxjava3/build.gradle
+++ b/work/work-rxjava3/build.gradle
@@ -32,7 +32,7 @@
}
androidx {
- name = "Android WorkManager RxJava3 Support"
+ name = "WorkManager RxJava3"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2020"
description = "Android WorkManager RxJava3 interoperatibility library"
diff --git a/work/work-testing/build.gradle b/work/work-testing/build.gradle
index 45e33ef..52a3ba4 100644
--- a/work/work-testing/build.gradle
+++ b/work/work-testing/build.gradle
@@ -44,7 +44,7 @@
}
androidx {
- name = "Android WorkManager Testing"
+ name = "WorkManager Testing"
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2018"
description = "Android WorkManager testing library"