Merge "Ignore flaky test" into androidx-main am: 0137117eb3
Original change: https://android-review.googlesource.com/c/platform/frameworks/support/+/2587366
Change-Id: Ib43d1f948f7a66b0c8260a15302842af1e1c2e0c
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/activity/activity/api/current.txt b/activity/activity/api/current.txt
index 22ecc41..636c36c 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);
@@ -95,8 +98,12 @@
public final class OnBackPressedDispatcher {
ctor public OnBackPressedDispatcher();
ctor public OnBackPressedDispatcher(optional Runnable? fallbackOnBackPressed);
+ ctor public OnBackPressedDispatcher(Runnable? fallbackOnBackPressed, androidx.core.util.Consumer<java.lang.Boolean>? onHasEnabledCallbacksChanged);
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 22ecc41..636c36c 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);
@@ -95,8 +98,12 @@
public final class OnBackPressedDispatcher {
ctor public OnBackPressedDispatcher();
ctor public OnBackPressedDispatcher(optional Runnable? fallbackOnBackPressed);
+ ctor public OnBackPressedDispatcher(Runnable? fallbackOnBackPressed, androidx.core.util.Consumer<java.lang.Boolean>? onHasEnabledCallbacksChanged);
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 10c4134..94f0f54 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);
@@ -94,8 +97,12 @@
public final class OnBackPressedDispatcher {
ctor public OnBackPressedDispatcher();
ctor public OnBackPressedDispatcher(optional Runnable? fallbackOnBackPressed);
+ ctor public OnBackPressedDispatcher(Runnable? fallbackOnBackPressed, androidx.core.util.Consumer<java.lang.Boolean>? onHasEnabledCallbacksChanged);
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/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/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/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 a3244de..1e295d6 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(androidx.appsearch.builtintypes.Alarm);
ctor public Alarm.Builder(String, String);
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(androidx.appsearch.builtintypes.AlarmInstance);
ctor public AlarmInstance.Builder(String, String, String);
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(androidx.appsearch.builtintypes.ContactPoint);
ctor public ContactPoint.Builder(String, String, String);
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(androidx.appsearch.builtintypes.properties.Keyword);
method public androidx.appsearch.builtintypes.ImageObject.Builder addKeyword(String);
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(androidx.appsearch.builtintypes.Person);
ctor public Person.Builder(String, String, String);
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(androidx.appsearch.builtintypes.Stopwatch);
ctor public Stopwatch.Builder(String, String);
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 @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) public androidx.appsearch.builtintypes.Stopwatch.Builder setBaseTimeMillis(android.content.Context, long, long);
method public androidx.appsearch.builtintypes.Stopwatch.Builder setBaseTimeMillis(long, long, int);
@@ -227,8 +254,10 @@
ctor public StopwatchLap.Builder(androidx.appsearch.builtintypes.StopwatchLap);
ctor public StopwatchLap.Builder(String, String);
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(androidx.appsearch.builtintypes.Thing);
ctor public Thing.Builder(String, String);
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(androidx.appsearch.builtintypes.Timer);
ctor public Timer.Builder(String, String);
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 @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 setBaseTimeMillis(long, long, int);
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 a3244de..1e295d6 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(androidx.appsearch.builtintypes.Alarm);
ctor public Alarm.Builder(String, String);
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(androidx.appsearch.builtintypes.AlarmInstance);
ctor public AlarmInstance.Builder(String, String, String);
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(androidx.appsearch.builtintypes.ContactPoint);
ctor public ContactPoint.Builder(String, String, String);
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(androidx.appsearch.builtintypes.properties.Keyword);
method public androidx.appsearch.builtintypes.ImageObject.Builder addKeyword(String);
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(androidx.appsearch.builtintypes.Person);
ctor public Person.Builder(String, String, String);
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(androidx.appsearch.builtintypes.Stopwatch);
ctor public Stopwatch.Builder(String, String);
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 @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) public androidx.appsearch.builtintypes.Stopwatch.Builder setBaseTimeMillis(android.content.Context, long, long);
method public androidx.appsearch.builtintypes.Stopwatch.Builder setBaseTimeMillis(long, long, int);
@@ -227,8 +254,10 @@
ctor public StopwatchLap.Builder(androidx.appsearch.builtintypes.StopwatchLap);
ctor public StopwatchLap.Builder(String, String);
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(androidx.appsearch.builtintypes.Thing);
ctor public Thing.Builder(String, String);
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(androidx.appsearch.builtintypes.Timer);
ctor public Timer.Builder(String, String);
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 @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 setBaseTimeMillis(long, long, int);
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 a9939c7..e402ed8 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(androidx.appsearch.builtintypes.Alarm);
ctor public Alarm.Builder(String, String);
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(androidx.appsearch.builtintypes.AlarmInstance);
ctor public AlarmInstance.Builder(String, String, String);
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(androidx.appsearch.builtintypes.ContactPoint);
ctor public ContactPoint.Builder(String, String, String);
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(androidx.appsearch.builtintypes.properties.Keyword);
method public androidx.appsearch.builtintypes.ImageObject.Builder addKeyword(String);
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(androidx.appsearch.builtintypes.Person);
ctor public Person.Builder(String, String, String);
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(androidx.appsearch.builtintypes.Stopwatch);
ctor public Stopwatch.Builder(String, String);
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 @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) public androidx.appsearch.builtintypes.Stopwatch.Builder setBaseTimeMillis(android.content.Context, long, long);
method public androidx.appsearch.builtintypes.Stopwatch.Builder setBaseTimeMillis(long, long, int);
@@ -229,8 +256,10 @@
ctor public StopwatchLap.Builder(androidx.appsearch.builtintypes.StopwatchLap);
ctor public StopwatchLap.Builder(String, String);
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(androidx.appsearch.builtintypes.Thing);
ctor public Thing.Builder(String, String);
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(androidx.appsearch.builtintypes.Timer);
ctor public Timer.Builder(String, String);
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 @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 setBaseTimeMillis(long, long, int);
method public androidx.appsearch.builtintypes.Timer.Builder setCreationTimestampMillis(long);
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-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..0f8c627 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();
@@ -106,7 +127,6 @@
JoinSpec joinSpec = new JoinSpec.Builder("childPropertyExpression")
.setNestedSearch("nestedQuery", nestedSearchSpec)
.setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_SUM_RANKING_SIGNAL)
- .setMaxJoinedResultCount(10)
.build();
searchSpec.setJoinSpec(joinSpec);
@@ -149,11 +169,10 @@
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);
- assertThat(joinSpecProto.getMaxJoinedChildCount()).isEqualTo(10);
JoinSpecProto.NestedSpecProto nestedSpecProto = joinSpecProto.getNestedSpec();
assertThat(nestedSpecProto.getSearchSpec().getQuery()).isEqualTo("nestedQuery");
@@ -161,6 +180,86 @@
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)
+ .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 +330,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);
@@ -240,6 +340,46 @@
}
@Test
+ public void testToResultSpecProtoWithJoinSpec() throws Exception {
+ SearchSpec nestedSearchSpec = new SearchSpec.Builder()
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP).build();
+
+ // Create a JoinSpec object and set it in the converter
+ JoinSpec joinSpec = new JoinSpec.Builder("childPropertyExpression")
+ .setNestedSearch("nestedQuery", nestedSearchSpec)
+ .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_SUM_RANKING_SIGNAL)
+ .setMaxJoinedResultCount(10)
+ .build();
+
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+ .setJoinSpec(joinSpec)
+ .setResultCountPerPage(123)
+ .setSnippetCount(234)
+ .setSnippetCountPerProperty(345)
+ .setMaxSnippetSize(456)
+ .build();
+
+ SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+ /*queryExpression=*/"query",
+ searchSpec,
+ /*prefixes=*/ImmutableSet.of(),
+ /*namespaceMap=*/ImmutableMap.of(),
+ /*schemaMap=*/ImmutableMap.of());
+
+ ResultSpecProto resultSpecProto = converter.toResultSpecProto(
+ /*namespaceMap=*/ImmutableMap.of(),
+ /*schemaMap=*/ImmutableMap.of());
+
+ assertThat(resultSpecProto.getNumPerPage()).isEqualTo(123);
+ assertThat(resultSpecProto.getSnippetSpec().getNumToSnippet()).isEqualTo(234);
+ assertThat(resultSpecProto.getSnippetSpec().getNumMatchesPerProperty()).isEqualTo(345);
+ assertThat(resultSpecProto.getSnippetSpec().getMaxWindowUtf32Length()).isEqualTo(456);
+ assertThat(resultSpecProto.getMaxJoinedChildrenPerParentToReturn()).isEqualTo(10);
+ }
+
+ @Test
public void testToResultSpecProto_groupByPackage() {
SearchSpec searchSpec = new SearchSpec.Builder()
.setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 5)
@@ -261,7 +401,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 +446,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 +469,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 +536,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 +549,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 +982,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 +1015,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 +1072,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..54421cf 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,17 @@
// 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())
+ .setAllowCircularSchemaDefinitions(
+ icingOptionsConfig.getAllowCircularSchemaDefinitions())
+ .build();
LogUtil.piiTrace(TAG, "Constructing IcingSearchEngine, request", options);
mIcingSearchEngineLocked = new IcingSearchEngine(options);
LogUtil.piiTrace(
@@ -1450,7 +1463,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 +1505,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 +1647,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 +2238,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..3c44609
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/DefaultIcingOptionsConfig.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.
+ */
+// @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;
+ }
+
+ @Override
+ public boolean getAllowCircularSchemaDefinitions() {
+ return true;
+ }
+}
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..7d72283
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java
@@ -0,0 +1,105 @@
+/*
+ * 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();
+
+ /**
+ * Whether to allow circular references between schema types for the schema definition.
+ *
+ * <p>Even when set to true, circular references are still not allowed in the following cases:
+ * 1. All edges of a cycle have index_nested_properties=true
+ * 2. One of the types in the cycle has a joinable property, or depends on a type with a
+ * joinable property.
+ */
+ boolean getAllowCircularSchemaDefinitions();
+}
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..934f48a 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,25 @@
}
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()));
protoBuilder.setJoinSpec(joinSpecProtoBuilder);
}
@@ -252,10 +323,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(
@@ -263,6 +338,11 @@
.setNumToSnippet(mSearchSpec.getSnippetCount())
.setNumMatchesPerProperty(mSearchSpec.getSnippetCountPerProperty())
.setMaxWindowUtf32Length(mSearchSpec.getMaxSnippetSize()));
+ JoinSpec joinSpec = mSearchSpec.getJoinSpec();
+ if (joinSpec != null) {
+ resultSpecBuilder.setMaxJoinedChildrenPerParentToReturn(
+ joinSpec.getMaxJoinedResultCount());
+ }
// Rewrites the typePropertyMasks that exist in {@code prefixes}.
int groupingType = mSearchSpec.getResultGroupingTypeFlags();
@@ -279,12 +359,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 +467,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 +545,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 +660,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 +793,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 +811,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..8d9c55f
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/api/current.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.appsearch.playservicesstorage {
+
+ public final class PlayServicesStorage {
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSessionAsync(androidx.appsearch.playservicesstorage.PlayServicesStorage.GlobalSearchContext);
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.playservicesstorage.PlayServicesStorage.SearchContext);
+ }
+
+ public static final class PlayServicesStorage.GlobalSearchContext {
+ }
+
+ public static final class PlayServicesStorage.GlobalSearchContext.Builder {
+ ctor public PlayServicesStorage.GlobalSearchContext.Builder(android.content.Context);
+ method public androidx.appsearch.playservicesstorage.PlayServicesStorage.GlobalSearchContext build();
+ }
+
+ public static final class PlayServicesStorage.SearchContext {
+ method public String getDatabaseName();
+ }
+
+ public static final class PlayServicesStorage.SearchContext.Builder {
+ ctor public PlayServicesStorage.SearchContext.Builder(android.content.Context, String);
+ method public androidx.appsearch.playservicesstorage.PlayServicesStorage.SearchContext build();
+ }
+
+}
+
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..8d9c55f
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/api/public_plus_experimental_current.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.appsearch.playservicesstorage {
+
+ public final class PlayServicesStorage {
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSessionAsync(androidx.appsearch.playservicesstorage.PlayServicesStorage.GlobalSearchContext);
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.playservicesstorage.PlayServicesStorage.SearchContext);
+ }
+
+ public static final class PlayServicesStorage.GlobalSearchContext {
+ }
+
+ public static final class PlayServicesStorage.GlobalSearchContext.Builder {
+ ctor public PlayServicesStorage.GlobalSearchContext.Builder(android.content.Context);
+ method public androidx.appsearch.playservicesstorage.PlayServicesStorage.GlobalSearchContext build();
+ }
+
+ public static final class PlayServicesStorage.SearchContext {
+ method public String getDatabaseName();
+ }
+
+ public static final class PlayServicesStorage.SearchContext.Builder {
+ ctor public PlayServicesStorage.SearchContext.Builder(android.content.Context, String);
+ method public androidx.appsearch.playservicesstorage.PlayServicesStorage.SearchContext build();
+ }
+
+}
+
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..8d9c55f
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/api/restricted_current.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.appsearch.playservicesstorage {
+
+ public final class PlayServicesStorage {
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSessionAsync(androidx.appsearch.playservicesstorage.PlayServicesStorage.GlobalSearchContext);
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.playservicesstorage.PlayServicesStorage.SearchContext);
+ }
+
+ public static final class PlayServicesStorage.GlobalSearchContext {
+ }
+
+ public static final class PlayServicesStorage.GlobalSearchContext.Builder {
+ ctor public PlayServicesStorage.GlobalSearchContext.Builder(android.content.Context);
+ method public androidx.appsearch.playservicesstorage.PlayServicesStorage.GlobalSearchContext build();
+ }
+
+ public static final class PlayServicesStorage.SearchContext {
+ method public String getDatabaseName();
+ }
+
+ public static final class PlayServicesStorage.SearchContext.Builder {
+ ctor public PlayServicesStorage.SearchContext.Builder(android.content.Context, String);
+ method public androidx.appsearch.playservicesstorage.PlayServicesStorage.SearchContext build();
+ }
+
+}
+
diff --git a/appsearch/appsearch-play-services-storage/build.gradle b/appsearch/appsearch-play-services-storage/build.gradle
new file mode 100644
index 0000000..61ec732
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/build.gradle
@@ -0,0 +1,51 @@
+/*
+ * 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")
+ implementation project(":core:core")
+ implementation("androidx.concurrent:concurrent-futures:1.0.0")
+ implementation('androidx.collection:collection:1.2.0')
+ implementation("com.google.android.gms:play-services-appsearch:16.0.0", {
+ exclude group: "androidx.fragment", module: "fragment"
+ exclude group: "androidx.core", module: "core"
+ })
+
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(libs.junit)
+}
+
+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-play-services-storage/src/androidTest/AndroidManifest.xml b/appsearch/appsearch-play-services-storage/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..0cd568b
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<!--
+ 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">
+</manifest>
\ No newline at end of file
diff --git a/appsearch/appsearch-play-services-storage/src/androidTest/java/androidx/appsearch/playservicesstorage/PlayServicesStorageTest.java b/appsearch/appsearch-play-services-storage/src/androidTest/java/androidx/appsearch/playservicesstorage/PlayServicesStorageTest.java
new file mode 100644
index 0000000..0f4db6d
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/src/androidTest/java/androidx/appsearch/playservicesstorage/PlayServicesStorageTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.playservicesstorage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Test;
+
+public class PlayServicesStorageTest {
+ @Test
+ public void testSearchContext_databaseName() {
+ PlayServicesStorage.SearchContext searchContext =
+ new PlayServicesStorage.SearchContext.Builder(
+ ApplicationProvider.getApplicationContext(),
+ /*databaseName=*/"dbName").build();
+
+ assertThat(searchContext.getDatabaseName()).isEqualTo("dbName");
+ }
+
+ @Test
+ public void testSearchContext_withInvalidDatabaseName() {
+ // Test special character can present in database name. When a special character is banned
+ // in database name, add checker in SearchContext.Builder and reflect it in java doc.
+
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+ () -> new PlayServicesStorage.SearchContext.Builder(
+ ApplicationProvider.getApplicationContext(),
+ "testDatabaseNameEndWith/").build());
+ assertThat(e).hasMessageThat().isEqualTo("Database name cannot contain '/'");
+ e = assertThrows(IllegalArgumentException.class,
+ () -> new PlayServicesStorage.SearchContext.Builder(
+ ApplicationProvider.getApplicationContext(),
+ "/testDatabaseNameStartWith").build());
+ assertThat(e).hasMessageThat().isEqualTo("Database name cannot contain '/'");
+ }
+}
diff --git a/appsearch/appsearch-play-services-storage/src/main/AndroidManifest.xml b/appsearch/appsearch-play-services-storage/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8a693a4
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?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 />
\ No newline at end of file
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
new file mode 100644
index 0000000..6953829
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
@@ -0,0 +1,33 @@
+/*
+ * 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.playservicesstorage;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.Features;
+
+/**
+ * An implementation of {@link Features}. Feature availability is dependent on Android API
+ * level and GMSCore AppSearch module.
+ */
+final class FeaturesImpl implements Features {
+ @Override
+ public boolean isFeatureSupported(@NonNull String feature) {
+ // TODO(b/274986359): Update based on features available in {@link Features} and those
+ // supported by play-services-appsearch.
+ return false;
+ }
+}
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/GlobalSearchSessionImpl.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/GlobalSearchSessionImpl.java
new file mode 100644
index 0000000..7dfacc9
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/GlobalSearchSessionImpl.java
@@ -0,0 +1,120 @@
+/*
+ * 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.playservicesstorage;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.Features;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.GlobalSearchSession;
+import androidx.appsearch.app.ReportSystemUsageRequest;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.observer.ObserverCallback;
+import androidx.appsearch.observer.ObserverSpec;
+import androidx.core.util.Preconditions;
+
+import com.google.android.gms.appsearch.GlobalSearchClient;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Executor;
+
+/**
+ * An implementation of {@link GlobalSearchSession} which proxies to a Google Play Service's
+ * {@link GlobalSearchClient}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class GlobalSearchSessionImpl implements GlobalSearchSession {
+
+ private final GlobalSearchClient mPlayServicesClient;
+ private final Features mFeatures;
+
+ GlobalSearchSessionImpl(
+ @NonNull GlobalSearchClient playServicesClient,
+ @NonNull Features features) {
+ mPlayServicesClient = Preconditions.checkNotNull(playServicesClient);
+ mFeatures = Preconditions.checkNotNull(features);
+ }
+ @NonNull
+ @Override
+ public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
+ @NonNull String packageName, @NonNull String databaseName,
+ @NonNull GetByDocumentIdRequest request) {
+ // TODO(b/274986359): Implement getByDocumentIdAsync for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "getByDocumentIdAsync is not yet supported on this AppSearch implementation.");
+ }
+
+ @NonNull
+ @Override
+ public SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
+ // TODO(b/274986359): Implement search for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "search is not yet supported on this AppSearch implementation.");
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<Void> reportSystemUsageAsync(
+ @NonNull ReportSystemUsageRequest request) {
+ // TODO(b/274986359): Implement reportSystemUsageAsync for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "reportSystemUsageAsync is not yet supported on this AppSearch implementation.");
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<GetSchemaResponse> getSchemaAsync(@NonNull String packageName,
+ @NonNull String databaseName) {
+ // TODO(b/274986359): Implement getSchemaAsync for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "getSchemaAsync is not yet supported on this AppSearch implementation.");
+ }
+
+ @NonNull
+ @Override
+ public Features getFeatures() {
+ return mFeatures;
+ }
+
+ @Override
+ public void registerObserverCallback(@NonNull String targetPackageName,
+ @NonNull ObserverSpec spec, @NonNull Executor executor,
+ @NonNull ObserverCallback observer) throws AppSearchException {
+ // TODO(b/274986359): Implement registerObserverCallback for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "registerObserverCallback is not yet supported on this AppSearch implementation.");
+ }
+
+ @Override
+ public void unregisterObserverCallback(@NonNull String targetPackageName,
+ @NonNull ObserverCallback observer) throws AppSearchException {
+ // TODO(b/274986359): Implement unregisterObserverCallback for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "unregisterObserverCallback is not yet supported on this "
+ + "AppSearch implementation.");
+ }
+
+ @Override
+ public void close() {
+ mPlayServicesClient.close();
+ }
+}
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/PlayServicesStorage.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/PlayServicesStorage.java
new file mode 100644
index 0000000..1829bcc
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/PlayServicesStorage.java
@@ -0,0 +1,157 @@
+/*
+ * 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.playservicesstorage;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GlobalSearchSession;
+import androidx.appsearch.playservicesstorage.util.AppSearchTaskFutures;
+import androidx.core.util.Preconditions;
+
+import com.google.android.gms.appsearch.AppSearch;
+import com.google.android.gms.appsearch.AppSearchClient;
+import com.google.android.gms.appsearch.GlobalSearchClient;
+import com.google.android.gms.tasks.Task;
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * An AppSearch storage system which stores data in the central AppSearch service in Google
+ * Play Services.
+ */
+public final class PlayServicesStorage {
+
+ private PlayServicesStorage() {
+ }
+
+ /** Contains information about how to create the search session. */
+ public static final class SearchContext {
+ final Context mContext;
+ final String mDatabaseName;
+
+ SearchContext(@NonNull Context context, @NonNull String databaseName) {
+ mContext = Preconditions.checkNotNull(context);
+ mDatabaseName = Preconditions.checkNotNull(databaseName);
+ }
+
+ /**
+ * Returns the name of the database to create or open.
+ */
+ @NonNull
+ public String getDatabaseName() {
+ return mDatabaseName;
+ }
+
+ /** Builder for {@link SearchContext} objects. */
+ public static final class Builder {
+ private final Context mContext;
+ private final String mDatabaseName;
+
+ /**
+ * Creates a {@link SearchContext.Builder} instance.
+ *
+ * <p>{@link AppSearchSession} will create or open a database under the given name.
+ *
+ * <p>Databases with different names are fully separate with distinct schema types,
+ * namespaces, and documents.
+ *
+ * <p>The database name cannot contain {@code '/'}.
+ *
+ * <p>The database name will be visible to all system UI or third-party applications
+ * that have been granted access to any of the database's documents (for example,
+ * using {@link
+ * androidx.appsearch.app.SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage}).
+ *
+ * @param databaseName The name of the database.
+ * @throws IllegalArgumentException if the databaseName contains {@code '/'}.
+ */
+ public Builder(@NonNull Context context, @NonNull String databaseName) {
+ mContext = Preconditions.checkNotNull(context);
+ Preconditions.checkNotNull(databaseName);
+ if (databaseName.contains("/")) {
+ throw new IllegalArgumentException("Database name cannot contain '/'");
+ }
+ mDatabaseName = databaseName;
+ }
+
+ /** Builds a {@link SearchContext} instance. */
+ @NonNull
+ public SearchContext build() {
+ return new SearchContext(mContext, mDatabaseName);
+ }
+ }
+ }
+
+ /** Contains information relevant to creating a global search session. */
+ public static final class GlobalSearchContext {
+ final Context mContext;
+
+ GlobalSearchContext(@NonNull Context context) {
+ mContext = Preconditions.checkNotNull(context);
+ }
+
+ /** Builder for {@link GlobalSearchContext} objects. */
+ public static final class Builder {
+ private final Context mContext;
+
+ public Builder(@NonNull Context context) {
+ mContext = Preconditions.checkNotNull(context);
+ }
+
+ /** Builds a {@link GlobalSearchContext} instance. */
+ @NonNull
+ public GlobalSearchContext build() {
+ return new GlobalSearchContext(mContext);
+ }
+ }
+ }
+
+ /**
+ * Opens a new {@link AppSearchSession} on this storage.
+ *
+ * @param context The {@link SearchContext} contains all information to create a new
+ * {@link AppSearchSession}
+ */
+ @NonNull
+ public static ListenableFuture<AppSearchSession> createSearchSessionAsync(
+ @NonNull SearchContext context) {
+ Preconditions.checkNotNull(context);
+ Task<AppSearchClient> appSearchClientTask = AppSearch
+ .createAppSearchClient(context.mContext);
+ return AppSearchTaskFutures.toListenableFuture(
+ appSearchClientTask,
+ task -> new SearchSessionImpl(task, new FeaturesImpl()));
+ }
+
+ /**
+ * Opens a new {@link GlobalSearchSession} on this storage.
+ *
+ * @param context The {@link GlobalSearchContext} contains all information to create a new
+ * {@link GlobalSearchSession}
+ */
+ @NonNull
+ public static ListenableFuture<GlobalSearchSession> createGlobalSearchSessionAsync(
+ @NonNull GlobalSearchContext context) {
+ Preconditions.checkNotNull(context);
+ Task<GlobalSearchClient> globalSearchClientTask = AppSearch
+ .createGlobalSearchClient(context.mContext);
+ return AppSearchTaskFutures.toListenableFuture(
+ globalSearchClientTask,
+ task -> new GlobalSearchSessionImpl(task, new FeaturesImpl()));
+ }
+}
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/SearchSessionImpl.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/SearchSessionImpl.java
new file mode 100644
index 0000000..49c076b
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/SearchSessionImpl.java
@@ -0,0 +1,173 @@
+/*
+ * 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.playservicesstorage;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.Features;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.RemoveByDocumentIdRequest;
+import androidx.appsearch.app.ReportUsageRequest;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.app.SearchSuggestionResult;
+import androidx.appsearch.app.SearchSuggestionSpec;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.app.StorageInfo;
+import androidx.core.util.Preconditions;
+
+import com.google.android.gms.appsearch.AppSearchClient;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * An implementation of {@link AppSearchSession} which proxies to a Google Play Service's
+ * {@link AppSearchClient}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class SearchSessionImpl implements AppSearchSession {
+ private final AppSearchClient mPlayServicesClient;
+ private final Features mFeatures;
+
+ SearchSessionImpl(
+ @NonNull AppSearchClient playServicesClient,
+ @NonNull Features features) {
+ mPlayServicesClient = Preconditions.checkNotNull(playServicesClient);
+ mFeatures = Preconditions.checkNotNull(features);
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<SetSchemaResponse> setSchemaAsync(@NonNull SetSchemaRequest request) {
+ // TODO(b/274986359): Implement setSchemaAsync for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "setSchemaAsync is not supported on this AppSearch implementation.");
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<GetSchemaResponse> getSchemaAsync() {
+ // TODO(b/274986359): Implement getSchemaAsync for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "getSchemaAsync is not supported on this AppSearch implementation.");
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<Set<String>> getNamespacesAsync() {
+ // TODO(b/274986359): Implement getNamespacesAsync for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "getNamespacesAsync is not supported on this AppSearch implementation.");
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<AppSearchBatchResult<String, Void>> putAsync(
+ @NonNull PutDocumentsRequest request) {
+ // TODO(b/274986359): Implement putAsync for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "putAsync is not supported on this AppSearch implementation.");
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
+ @NonNull GetByDocumentIdRequest request) {
+ // TODO(b/274986359): Implement getByDocumentIdAsync for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "getByDocumentIdAsync is not supported on this AppSearch implementation.");
+ }
+
+ @NonNull
+ @Override
+ public SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
+ // TODO(b/274986359): Implement search for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "search is not supported on this AppSearch implementation.");
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<List<SearchSuggestionResult>> searchSuggestionAsync(
+ @NonNull String suggestionQueryExpression,
+ @NonNull SearchSuggestionSpec searchSuggestionSpec) {
+ // TODO(b/274986359): Implement searchSuggestionAsync for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "searchSuggestionAsync is not supported on this AppSearch implementation.");
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<Void> reportUsageAsync(@NonNull ReportUsageRequest request) {
+ // TODO(b/274986359): Implement reportUsageAsync for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "reportUsageAsync is not supported on this AppSearch implementation.");
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<AppSearchBatchResult<String, Void>> removeAsync(
+ @NonNull RemoveByDocumentIdRequest request) {
+ // TODO(b/274986359): Implement removeAsync for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "removeAsync is not supported on this AppSearch implementation.");
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<Void> removeAsync(@NonNull String queryExpression,
+ @NonNull SearchSpec searchSpec) {
+ // TODO(b/274986359): Implement removeAsync for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "removeAsync is not supported on this AppSearch implementation.");
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<StorageInfo> getStorageInfoAsync() {
+ // TODO(b/274986359): Implement getStorageInfoAsync for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "getStorageInfoAsync is not supported on this AppSearch implementation.");
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<Void> requestFlushAsync() {
+ // TODO(b/274986359): Implement getSchemaAsync for PlayServicesStorage.
+ throw new UnsupportedOperationException(
+ "requestFlushAsync is not supported on this AppSearch implementation.");
+ }
+
+ @NonNull
+ @Override
+ public Features getFeatures() {
+ return mFeatures;
+ }
+
+ @Override
+ public void close() {
+ mPlayServicesClient.close();
+ }
+}
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/util/AppSearchTaskFutures.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/util/AppSearchTaskFutures.java
new file mode 100644
index 0000000..61a4332
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/util/AppSearchTaskFutures.java
@@ -0,0 +1,87 @@
+/*
+ * 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.playservicesstorage.util;
+
+import static androidx.appsearch.app.AppSearchResult.RESULT_NOT_FOUND;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.util.LogUtil;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.core.util.Function;
+
+import com.google.android.gms.tasks.Task;
+import com.google.common.util.concurrent.ListenableFuture;
+
+/** Utilities for converting {@link Task} to {@link ListenableFuture}. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class AppSearchTaskFutures {
+
+ private static final String TAG = "AppSearchTaskFutures";
+
+ private AppSearchTaskFutures() {}
+
+ /**
+ * Returns the error result code associated with the exception.
+ */
+ @NonNull
+ public static <GmsType, JetpackType> ListenableFuture<JetpackType> toListenableFuture(
+ @NonNull Task<GmsType> task,
+ @NonNull Function<GmsType, JetpackType> valueMapper) {
+ return CallbackToFutureAdapter.getFuture(
+ completer -> task.addOnCompleteListener(
+ completedTask -> {
+ if (completedTask.isCanceled()) {
+ completer.setCancelled();
+ } else if (completedTask.isSuccessful()) {
+ JetpackType jetpackType = valueMapper.apply(
+ completedTask.getResult());
+ completer.set(jetpackType);
+ } else {
+ Exception exception = task.getException();
+ completer.setException(toJetpackException(exception));
+ }
+ }));
+ }
+
+ private static Exception toJetpackException(Exception exception) {
+ if (exception instanceof com.google.android.gms.appsearch.exceptions.AppSearchException) {
+ com.google.android.gms.appsearch.exceptions.AppSearchException
+ gmsException =
+ (com.google.android.gms.appsearch.exceptions.AppSearchException) exception;
+ if (gmsException.getResultCode() == RESULT_NOT_FOUND && LogUtil.DEBUG) {
+ // Log for traceability. NOT_FOUND is logged at VERBOSE because this
+ // error can occur during the regular operation of the system
+ // (b/183550974). Everything else is indicative of an actual
+ // problem and is logged at WARN.
+ Log.v(TAG, "Failed to call PlayServicesAppSearch: "
+ + exception);
+ }
+
+ return new AppSearchException(
+ gmsException.getResultCode(),
+ gmsException.getMessage());
+ }
+
+ Log.w(TAG, "Failed to call PlayServicesAppSearch", exception);
+ return exception;
+ }
+
+}
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 7b7f92e..61fa4ed 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 7b7f92e..61fa4ed 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 7b7f92e..61fa4ed 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 f991e23..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')
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..59cad2a 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,8 +4567,11 @@
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).getRankingSignal()).isEqualTo(1.0);
+ assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);
+ // SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child documents
+ // returned. It does not affect the number of child documents that are scored. So the score
+ // (the COUNT of the number of children) is 2, even though only one child is returned.
+ assertThat(sr.get(0).getRankingSignal()).isEqualTo(2.0);
assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id2");
assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
@@ -4239,26 +4579,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 +4660,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 +4724,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 +4800,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 +4879,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 +4932,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 +4999,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 +5052,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 +5103,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 +5144,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..5fadd15 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
@@ -29,14 +29,21 @@
import androidx.annotation.NonNull;
import androidx.appsearch.app.AppSearchBatchResult;
import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
import androidx.appsearch.app.AppSearchSession;
import androidx.appsearch.app.Features;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.JoinSpec;
import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.SearchResult;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.app.SetSchemaRequest;
import androidx.appsearch.platformstorage.PlatformStorage;
import androidx.appsearch.testutil.AppSearchEmail;
+import androidx.appsearch.util.DocumentIdUtil;
import androidx.core.os.BuildCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SdkSuppress;
@@ -160,63 +167,106 @@
// 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.
}
+ // TODO(b/256022027) Remove this overridden test once the change to setMaxJoinedResultCount
+ // is synced over into framework.
@Override
@Test
- public void testSearchSuggestion_namespaceFilter() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
+ public void testSimpleJoin() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+ AppSearchSession db = PlatformStorage.createSearchSessionAsync(
+ new PlatformStorage.SearchContext.Builder(context, DB_NAME_1).build()).get();
+ assumeTrue(db.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
- @Override
- @Test
- public void testSearchSuggestion_documentIdFilter() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
+ // 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();
- @Override
- @Test
- public void testSearchSuggestion_differentPrefix() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
+ // Schema registration
+ db.setSchemaAsync(
+ new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
+ .build()).get();
- @Override
- @Test
- public void testSearchSuggestion_differentRankingStrategy() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
+ // Index a document
+ // While inEmail2 has a higher document score, we will rank based on the number of joined
+ // documents. inEmail1 will have 1 joined document while inEmail2 will have 0 joined
+ // documents.
+ 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();
- @Override
- @Test
- public void testSearchSuggestion_removeDocument() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
+ AppSearchEmail inEmail2 =
+ new AppSearchEmail.Builder("namespace", "id2")
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .setScore(10)
+ .build();
- @Override
- @Test
- public void testSearchSuggestion_replacementDocument() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
+ String qualifiedId = DocumentIdUtil.createQualifiedId(context.getPackageName(), DB_NAME_1,
+ "namespace", "id1");
+ GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
+ .setScore(1)
+ .setPropertyString("entityId", qualifiedId)
+ .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(db.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
+ viewAction1, viewAction2)
+ .build()));
- @Override
- @Test
- public void testSearchSuggestion_ignoreOperators() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
+ SearchSpec nestedSearchSpec =
+ new SearchSpec.Builder()
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
+ .setOrder(SearchSpec.ORDER_ASCENDING)
+ .build();
- @Override
- @Test
- public void testSearchSuggestion_schemaFilter() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
- }
+ JoinSpec js = new JoinSpec.Builder("entityId")
+ .setNestedSearch("", nestedSearchSpec)
+ .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+ .setMaxJoinedResultCount(1)
+ .build();
- @Override
- @Test
- public void testSearchSuggestion_twoInstances() throws Exception {
- // TODO(b/227356108) enable the test when suggestion is ready in platform.
+ SearchResults searchResults = db.search("body email", new SearchSpec.Builder()
+ .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+ .setJoinSpec(js)
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .build());
+
+ List<SearchResult> sr = searchResults.getNextPageAsync().get();
+
+ // Both email docs are returned, but id1 comes first due to the join
+ assertThat(sr).hasSize(2);
+
+ assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id1");
+ assertThat(sr.get(0).getJoinedResults()).hasSize(1);
+ 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");
+ assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
+ assertThat(sr.get(1).getJoinedResults()).isEmpty();
}
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
index 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..71daa7f 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()";
/**
@@ -177,8 +181,8 @@
}
/**
- * Returns the max amount of {@link SearchResult} objects that will be joined to the parent
- * document, with a default of 10 SearchResults.
+ * Returns the max amount of {@link SearchResult} objects to return with the parent document,
+ * with a default of 10 SearchResults.
*/
public int getMaxJoinedResultCount() {
return mBundle.getInt(MAX_JOINED_RESULT_COUNT);
@@ -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) {
@@ -274,9 +280,15 @@
}
/**
- * Sets the max amount of {@link SearchResults} to join to the parent document, with a
+ * Sets the max amount of {@link SearchResults} to return with the parent document, with a
* default of 10 SearchResults.
+ *
+ * <p>This does NOT limit the number of results that are joined with the parent
+ * document for scoring. This means that, when set, only a maximum of
+ * {@code maxJoinedResultCount} results will be returned with each parent document, but
+ * all results that are joined with a parent will factor into the score.
*/
+ @CanIgnoreReturnValue
@NonNull
public Builder setMaxJoinedResultCount(int maxJoinedResultCount) {
mMaxJoinedResultCount = maxJoinedResultCount;
@@ -292,6 +304,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/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/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/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/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/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/public/src/main/kotlin/androidx/build/SupportConfig.kt b/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
index 47d27e4..d9304a9 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/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/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/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-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/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/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/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/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/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/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index 75258ec..9856926 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -126,7 +126,7 @@
* The maven version string of this compiler. This string should be updated before/after every
* release.
*/
- const val compilerVersion: String = "1.5.0-alpha05"
+ const val compilerVersion: String = "1.4.7"
private val minimumRuntimeVersion: String
get() = runtimeVersionToMavenVersionTable[minimumRuntimeVersionInt] ?: "unknown"
}
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/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/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/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/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/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-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-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-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..13c0b67
--- /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.CALL_TYPE_VIDEO_CALL
+import androidx.core.telecom.CallAttributesCompat.Companion.DIRECTION_INCOMING
+import androidx.core.telecom.CallAttributesCompat.Companion.DIRECTION_OUTGOING
+
+@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/BaseTelecomTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/BaseTelecomTest.kt
new file mode 100644
index 0000000..a044deb
--- /dev/null
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/BaseTelecomTest.kt
@@ -0,0 +1,59 @@
+/*
+ * 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
+import android.telecom.PhoneAccountHandle
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.internal.JetpackConnectionService
+import androidx.core.telecom.internal.utils.Utils
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.TestExecutor
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.asCoroutineDispatcher
+import org.junit.After
+import org.junit.Before
+
+@RequiresApi(Build.VERSION_CODES.O)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O /* api=26 */)
+abstract class BaseTelecomTest {
+ val mContext: Context = ApplicationProvider.getApplicationContext()
+ val mWorkerExecutor = TestExecutor()
+ val mWorkerContext: CoroutineContext = mWorkerExecutor.asCoroutineDispatcher()
+
+ lateinit var mCallsManager: CallsManager
+ lateinit var mPackagePhoneAccountHandle: PhoneAccountHandle
+ internal lateinit var mConnectionService: JetpackConnectionService
+
+ @Before
+ fun setUpBase() {
+ Utils.resetUtils()
+ mCallsManager = CallsManager(mContext)
+ mConnectionService = mCallsManager.mConnectionService
+ mCallsManager.registerAppWithTelecom(CallsManager.CAPABILITY_BASELINE)
+ mPackagePhoneAccountHandle = mCallsManager.getPhoneAccountHandleForPackage()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @After
+ fun onDestroyBase() {
+ Utils.resetUtils()
+ JetpackConnectionService.mPendingConnectionRequests.clear()
+ }
+}
\ No newline at end of file
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..1f0a43a
--- /dev/null
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/CallsManagerTest.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.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.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 : BaseTelecomTest() {
+ private val mTestClassName = "androidx.core.telecom.test"
+
+ @SmallTest
+ @Test
+ fun testGetPhoneAccountWithUBuild() {
+ try {
+ Utils.setUtils(TestUtils.mV2Build)
+ val account = mCallsManager.getPhoneAccountHandleForPackage()
+ assertEquals(mTestClassName, account.componentName.className)
+ } finally {
+ Utils.resetUtils()
+ }
+ }
+
+ @SmallTest
+ @Test
+ fun testGetPhoneAccountWithUBuildWithTminusBuild() {
+ try {
+ Utils.setUtils(TestUtils.mBackwardsCompatBuild)
+ val account = mCallsManager.getPhoneAccountHandleForPackage()
+ assertEquals(CallsManager.CONNECTION_SERVICE_CLASS, account.componentName.className)
+ } finally {
+ Utils.resetUtils()
+ }
+ }
+
+ @SmallTest
+ @Test
+ fun testGetPhoneAccountWithInvalidBuild() {
+ try {
+ Utils.setUtils(TestUtils.mInvalidBuild)
+ assertThrows(UnsupportedOperationException::class.java) {
+ mCallsManager.getPhoneAccountHandleForPackage()
+ }
+ } finally {
+ Utils.resetUtils()
+ }
+ }
+
+ @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..6f7bb4a
--- /dev/null
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/JetpackConnectionServiceTest.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.telecom.Connection
+import android.telecom.ConnectionRequest
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.TestUtils.Companion.TEST_CALL_ATTRIB_NAME
+import androidx.core.telecom.TestUtils.Companion.TEST_CALL_ATTRIB_NUMBER
+import androidx.core.telecom.internal.CallChannels
+import androidx.core.telecom.internal.JetpackConnectionService
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+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 : BaseTelecomTest() {
+ private val callChannels = CallChannels()
+
+ @After
+ fun onDestroy() {
+ super.onDestroyBase()
+ callChannels.closeAllChannels()
+ }
+
+ /**
+ * 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 = TestUtils.createCallAttributes(
+ CallAttributesCompat.DIRECTION_OUTGOING,
+ mPackagePhoneAccountHandle
+ )
+ // 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 = TestUtils.createCallAttributes(
+ CallAttributesCompat.DIRECTION_INCOMING,
+ mPackagePhoneAccountHandle
+ )
+ // 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 createConnectionRequest(callAttributesCompat: CallAttributesCompat):
+ ConnectionRequest {
+ // wrap in PendingRequest
+ val pr = JetpackConnectionService.PendingConnectionRequest(
+ callAttributesCompat, callChannels, mWorkerContext, null
+ )
+ // add to the list of pendingRequests
+ JetpackConnectionService.mPendingConnectionRequests.add(pr)
+ // create a ConnectionRequest
+ return ConnectionRequest(mPackagePhoneAccountHandle, TEST_CALL_ATTRIB_NUMBER, null)
+ }
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/TestUtils.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/TestUtils.kt
new file mode 100644
index 0000000..d37080f
--- /dev/null
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/TestUtils.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
+
+import android.net.Uri
+import android.telecom.PhoneAccountHandle
+import androidx.core.telecom.internal.utils.BuildVersionAdapter
+
+class TestUtils {
+
+ companion object {
+
+ internal val mV2Build = object : BuildVersionAdapter {
+ override fun hasPlatformV2Apis(): Boolean {
+ return true
+ }
+
+ override fun hasInvalidBuildVersion(): Boolean {
+ return false
+ }
+ }
+
+ internal val mBackwardsCompatBuild = object : BuildVersionAdapter {
+ override fun hasPlatformV2Apis(): Boolean {
+ return false
+ }
+
+ override fun hasInvalidBuildVersion(): Boolean {
+ return false
+ }
+ }
+
+ internal val mInvalidBuild = object : BuildVersionAdapter {
+ override fun hasPlatformV2Apis(): Boolean {
+ return false
+ }
+
+ override fun hasInvalidBuildVersion(): Boolean {
+ return true
+ }
+ }
+
+ val TEST_CALL_ATTRIB_NAME = "Elon Musk"
+ val TEST_CALL_ATTRIB_NUMBER = Uri.parse("tel:6506959001")
+
+ fun createCallAttributes(
+ callDirection: Int,
+ phoneAccountHandle: PhoneAccountHandle,
+ 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 = phoneAccountHandle
+
+ return attributes
+ }
+ }
+}
\ 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..4ab0ec2
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/Utils.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.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 android.util.Log
+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 TAG = Utils.Companion::class.java.simpleName
+
+ 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 {
+ Log.i(TAG, "hasPlatformV2Apis: " +
+ "versionSdkInt=[${VERSION.SDK_INT}]")
+ return VERSION.SDK_INT >= 34 || VERSION.CODENAME == "UpsideDownCake"
+ }
+
+ override fun hasInvalidBuildVersion(): Boolean {
+ Log.i(TAG, "hasInvalidBuildVersion: " +
+ "versionSdkInt=[${VERSION.SDK_INT}]")
+ 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/api/current.txt b/core/core/api/current.txt
index be1967c..ad2bb35 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, android.content.ComponentName, int, android.content.Intent);
@@ -794,6 +803,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();
@@ -954,6 +964,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
@@ -1107,7 +1118,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);
@@ -2050,6 +2060,26 @@
}
+package androidx.core.service.quicksettings {
+
+ public class PendingIntentActivityWrapper {
+ ctor public PendingIntentActivityWrapper(android.content.Context, int, android.content.Intent, int, android.os.Bundle?, boolean);
+ ctor public PendingIntentActivityWrapper(android.content.Context, int, android.content.Intent, int, 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 {
@@ -2198,6 +2228,66 @@
method public static void addLinks(android.widget.TextView, 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(boolean);
+ method public static String getCalendarType(java.util.Locale);
+ method public static String getCalendarType(java.util.Locale, boolean);
+ method public static String getFirstDayOfWeek();
+ method public static String getFirstDayOfWeek(boolean);
+ method public static String getFirstDayOfWeek(java.util.Locale);
+ method public static String getFirstDayOfWeek(java.util.Locale, boolean);
+ method public static String getHourCycle();
+ method public static String getHourCycle(boolean);
+ method public static String getHourCycle(java.util.Locale);
+ method public static String getHourCycle(java.util.Locale, boolean);
+ method public static String getTemperatureUnit();
+ method public static String getTemperatureUnit(boolean);
+ method public static String getTemperatureUnit(java.util.Locale);
+ 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 {
@@ -2217,6 +2307,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!...);
@@ -2279,6 +2373,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 {
@@ -2767,9 +2869,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 {
@@ -3294,6 +3399,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
@@ -3476,6 +3582,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";
@@ -3557,6 +3664,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;
@@ -3729,6 +3837,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 0c385e3..7d63299 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, android.content.ComponentName, int, android.content.Intent);
@@ -794,6 +803,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();
@@ -954,6 +964,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
@@ -1107,7 +1118,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);
@@ -2057,6 +2067,26 @@
}
+package androidx.core.service.quicksettings {
+
+ public class PendingIntentActivityWrapper {
+ ctor public PendingIntentActivityWrapper(android.content.Context, int, android.content.Intent, int, android.os.Bundle?, boolean);
+ ctor public PendingIntentActivityWrapper(android.content.Context, int, android.content.Intent, int, 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 {
@@ -2205,6 +2235,66 @@
method public static void addLinks(android.widget.TextView, 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(boolean);
+ method public static String getCalendarType(java.util.Locale);
+ method public static String getCalendarType(java.util.Locale, boolean);
+ method public static String getFirstDayOfWeek();
+ method public static String getFirstDayOfWeek(boolean);
+ method public static String getFirstDayOfWeek(java.util.Locale);
+ method public static String getFirstDayOfWeek(java.util.Locale, boolean);
+ method public static String getHourCycle();
+ method public static String getHourCycle(boolean);
+ method public static String getHourCycle(java.util.Locale);
+ method public static String getHourCycle(java.util.Locale, boolean);
+ method public static String getTemperatureUnit();
+ method public static String getTemperatureUnit(boolean);
+ method public static String getTemperatureUnit(java.util.Locale);
+ 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 {
@@ -2224,6 +2314,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!...);
@@ -2286,6 +2380,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 {
@@ -2774,9 +2876,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 {
@@ -3301,6 +3406,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
@@ -3483,6 +3589,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";
@@ -3564,6 +3671,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;
@@ -3736,6 +3844,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 5f1829e..2172a82 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -203,6 +203,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, android.content.ComponentName, int, android.content.Intent);
@@ -892,6 +901,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();
@@ -1073,6 +1083,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
@@ -1229,7 +1240,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);
@@ -2427,6 +2437,26 @@
}
+package androidx.core.service.quicksettings {
+
+ public class PendingIntentActivityWrapper {
+ ctor public PendingIntentActivityWrapper(android.content.Context, int, android.content.Intent, int, android.os.Bundle?, boolean);
+ ctor public PendingIntentActivityWrapper(android.content.Context, int, android.content.Intent, int, 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 {
@@ -2580,6 +2610,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(boolean);
+ method public static String getCalendarType(java.util.Locale);
+ method public static String getCalendarType(java.util.Locale, boolean);
+ method public static String getFirstDayOfWeek();
+ method public static String getFirstDayOfWeek(boolean);
+ method public static String getFirstDayOfWeek(java.util.Locale);
+ method public static String getFirstDayOfWeek(java.util.Locale, boolean);
+ method public static String getHourCycle();
+ method public static String getHourCycle(boolean);
+ method public static String getHourCycle(java.util.Locale);
+ method public static String getHourCycle(java.util.Locale, boolean);
+ method public static String getTemperatureUnit();
+ method public static String getTemperatureUnit(boolean);
+ method public static String getTemperatureUnit(java.util.Locale);
+ 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 {
@@ -2603,6 +2693,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();
@@ -2703,6 +2797,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 {
@@ -3216,9 +3318,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 {
@@ -3769,6 +3877,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
@@ -3956,6 +4065,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";
@@ -4041,6 +4151,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;
@@ -4215,6 +4326,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 832085f..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"))
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 c1e0eda..a0ce095 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;
@@ -531,11 +532,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/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 a783ea6..e579c2b 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 d4dc2c5..fabbf08 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/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 5f71623..9d9486a 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);
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
+ 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);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
- 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);
+ ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
+ ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
+ ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
+ }
+
+ public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {
+ ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);
+ }
+
public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
ctor public CreatePasswordRequest(String id, String password);
ctor public CreatePasswordRequest(String id, String password, optional String? origin);
+ ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials);
method public String getId();
method public String getPassword();
property public final String id;
@@ -61,14 +69,13 @@
public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
ctor public CreatePublicKeyCredentialRequest(String requestJson);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
- method public String? getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, 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);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);
+ ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
+ ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
+ ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
method public String? getOrigin();
+ method public boolean getPreferIdentityDocUi();
+ method public 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 {
@@ -145,34 +183,25 @@
public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed);
- 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;
+ 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);
}
public final class GetPasswordOption extends androidx.credentials.CredentialOption {
ctor public GetPasswordOption();
- ctor public GetPasswordOption(optional boolean isAutoSelectAllowed);
- method public boolean isAutoSelectAllowed();
- property public boolean isAutoSelectAllowed;
+ ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds);
+ ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed);
+ ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
+ method public java.util.Set<java.lang.String> getAllowedUserIds();
+ property public final java.util.Set<java.lang.String> allowedUserIds;
}
public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
ctor public GetPublicKeyCredentialOption(String requestJson);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional String? clientDataHash);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- method public String? getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set<android.content.ComponentName> allowedProviders);
+ method public byte[]? getClientDataHash();
method public String getRequestJson();
- property public final 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);
+ ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, android.service.credentials.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);
+ method public byte[]? getClientDataHash();
+ method public String getRequestJson();
+ property public final byte[]? clientDataHash;
+ property public final String requestJson;
+ }
+
+ public abstract class BeginGetCredentialOption {
+ method public final android.os.Bundle getCandidateQueryData();
+ method public final String getId();
+ method public final String getType();
+ property public final android.os.Bundle candidateQueryData;
+ property public final String id;
+ property public final String type;
+ }
+
+ public final class BeginGetCredentialRequest {
+ ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions);
+ ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions, optional android.service.credentials.CallingAppInfo? callingAppInfo);
+ 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);
+ ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson, optional byte[]? clientDataHash);
+ method public byte[]? getClientDataHash();
+ method public String getRequestJson();
+ property public final byte[]? clientDataHash;
+ property public final String requestJson;
+ }
+
+ public final class 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 5f71623..9d9486a 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);
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
+ 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);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
- 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);
+ ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
+ ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
+ ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
+ }
+
+ public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {
+ ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);
+ }
+
public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
ctor public CreatePasswordRequest(String id, String password);
ctor public CreatePasswordRequest(String id, String password, optional String? origin);
+ ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials);
method public String getId();
method public String getPassword();
property public final String id;
@@ -61,14 +69,13 @@
public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
ctor public CreatePublicKeyCredentialRequest(String requestJson);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
- method public String? getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, 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);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);
+ ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
+ ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
+ ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
method public String? getOrigin();
+ method public boolean getPreferIdentityDocUi();
+ method public 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 {
@@ -145,34 +183,25 @@
public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed);
- 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;
+ 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);
}
public final class GetPasswordOption extends androidx.credentials.CredentialOption {
ctor public GetPasswordOption();
- ctor public GetPasswordOption(optional boolean isAutoSelectAllowed);
- method public boolean isAutoSelectAllowed();
- property public boolean isAutoSelectAllowed;
+ ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds);
+ ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed);
+ ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
+ method public java.util.Set<java.lang.String> getAllowedUserIds();
+ property public final java.util.Set<java.lang.String> allowedUserIds;
}
public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
ctor public GetPublicKeyCredentialOption(String requestJson);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional String? clientDataHash);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- method public String? getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set<android.content.ComponentName> allowedProviders);
+ method public byte[]? getClientDataHash();
method public String getRequestJson();
- property public final 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);
+ ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, android.service.credentials.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);
+ method public byte[]? getClientDataHash();
+ method public String getRequestJson();
+ property public final byte[]? clientDataHash;
+ property public final String requestJson;
+ }
+
+ public abstract class BeginGetCredentialOption {
+ method public final android.os.Bundle getCandidateQueryData();
+ method public final String getId();
+ method public final String getType();
+ property public final android.os.Bundle candidateQueryData;
+ property public final String id;
+ property public final String type;
+ }
+
+ public final class BeginGetCredentialRequest {
+ ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions);
+ ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions, optional android.service.credentials.CallingAppInfo? callingAppInfo);
+ 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);
+ ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson, optional byte[]? clientDataHash);
+ method public byte[]? getClientDataHash();
+ method public String getRequestJson();
+ property public final byte[]? clientDataHash;
+ property public final String requestJson;
+ }
+
+ public final class 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 5f71623..9d9486a 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);
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
+ 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);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
- 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);
+ ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
+ ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
+ ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
+ }
+
+ public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {
+ ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);
+ }
+
public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
ctor public CreatePasswordRequest(String id, String password);
ctor public CreatePasswordRequest(String id, String password, optional String? origin);
+ ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials);
method public String getId();
method public String getPassword();
property public final String id;
@@ -61,14 +69,13 @@
public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
ctor public CreatePublicKeyCredentialRequest(String requestJson);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
- method public String? getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, 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);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);
+ ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
+ ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
+ ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
method public String? getOrigin();
+ method public boolean getPreferIdentityDocUi();
+ method public 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 {
@@ -145,34 +183,25 @@
public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed);
- 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;
+ 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);
}
public final class GetPasswordOption extends androidx.credentials.CredentialOption {
ctor public GetPasswordOption();
- ctor public GetPasswordOption(optional boolean isAutoSelectAllowed);
- method public boolean isAutoSelectAllowed();
- property public boolean isAutoSelectAllowed;
+ ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds);
+ ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed);
+ ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
+ method public java.util.Set<java.lang.String> getAllowedUserIds();
+ property public final java.util.Set<java.lang.String> allowedUserIds;
}
public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
ctor public GetPublicKeyCredentialOption(String requestJson);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional String? clientDataHash);
- ctor public GetPublicKeyCredentialOption(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- method public String? getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set<android.content.ComponentName> allowedProviders);
+ method public byte[]? getClientDataHash();
method public String getRequestJson();
- property public final 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);
+ ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, android.service.credentials.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);
+ method public byte[]? getClientDataHash();
+ method public String getRequestJson();
+ property public final byte[]? clientDataHash;
+ property public final String requestJson;
+ }
+
+ public abstract class BeginGetCredentialOption {
+ method public final android.os.Bundle getCandidateQueryData();
+ method public final String getId();
+ method public final String getType();
+ property public final android.os.Bundle candidateQueryData;
+ property public final String id;
+ property public final String type;
+ }
+
+ public final class BeginGetCredentialRequest {
+ ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions);
+ ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions, optional android.service.credentials.CallingAppInfo? callingAppInfo);
+ 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);
+ ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson, optional byte[]? clientDataHash);
+ method public byte[]? getClientDataHash();
+ method public String getRequestJson();
+ property public final byte[]? clientDataHash;
+ property public final String requestJson;
+ }
+
+ public final class 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 c548aa0..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 {
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 1f477d4..cfc3aeb 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt
@@ -16,6 +16,9 @@
package androidx.credentials
+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
@@ -53,6 +56,9 @@
)
}
assertThat(request.origin).isEqualTo(origin)
+ assertThat(request.preferIdentityDocUi).isFalse()
+ assertThat(request.preferImmediatelyAvailableCredentials).isFalse()
+ assertThat(request.preferUiBrandingComponentName).isNull()
}
@Test
@@ -65,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
@@ -102,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())
@@ -112,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..80a9a2c1
--- /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.core.os.BuildCompat
+import androidx.credentials.R
+import androidx.credentials.equals
+import androidx.credentials.provider.BeginGetCredentialOption
+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..992c1d0
--- /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_CLIENT_DATA_HASH
+import androidx.credentials.CreatePublicKeyCredentialRequest.Companion.BUNDLE_KEY_REQUEST_JSON
+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..0669c20
--- /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.annotation.RequiresApi
+import androidx.credentials.PasswordCredential
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.internal.FrameworkClassParsingException
+import androidx.credentials.provider.BeginCreateCredentialRequest
+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..fb7735e
--- /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.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.credentials.provider.Action
+import androidx.credentials.provider.AuthenticationAction
+import androidx.credentials.provider.BeginGetCredentialOption
+import androidx.credentials.provider.BeginGetCredentialRequest
+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/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/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 4884c02..5d682e7 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -1018,6 +1018,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;
@@ -1047,4 +1051,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 b2b8c41..e2947aa 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/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/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/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-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/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 ae6d975..e5df997 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -24,14 +24,14 @@
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
# Comma-delimited lists of project path prefixes which have been opted-out of or opted-in to the
# Suppress Compatibility migration. Opt-out is matched first.
@@ -39,7 +39,7 @@
androidx.suppress.compatibility.optin=core
# 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 5eab30c..47505ad 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 d7e7ba8..4d39613 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 d7e7ba8..4d39613 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 4a220ca..2c15952 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 b99b3c8..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)
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/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/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 460f50a..d236c1e 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 8a303de..c9f06f1 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..902f803
--- /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 next();
+ method public androidx.graphics.path.PathSegment.Type next(float[] points);
+ method public androidx.graphics.path.PathSegment.Type next(float[] points, optional int offset);
+ 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/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index 138be5e..57a0469 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, java.util.List<java.lang.String> recordIdsList, java.util.List<java.lang.String> clientRecordIdsList, 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);
- method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
method public androidx.health.connect.client.PermissionController getPermissionController();
- method public default static int getSdkStatus(android.content.Context context);
- method public default static int getSdkStatus(android.content.Context context, optional String providerPackageName);
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);
- method @Deprecated public default static boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
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);
- method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
- method public int getSdkStatus(android.content.Context context);
- method public int getSdkStatus(android.content.Context context, optional String providerPackageName);
method @Deprecated public boolean isApiSupported();
- method @Deprecated public boolean isProviderAvailable(android.content.Context context);
- method @Deprecated public boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
- 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();
- 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 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();
- method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract(optional String providerPackageName);
}
}
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 138be5e..4eaa66c 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, java.util.List<java.lang.String> recordIdsList, java.util.List<java.lang.String> clientRecordIdsList, 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);
- method 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 String getHealthConnectSettingsAction();
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
method public androidx.health.connect.client.PermissionController getPermissionController();
- method public default static int getSdkStatus(android.content.Context context);
- method 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 @androidx.core.os.BuildCompat.PrereleaseSdkCheck public default static int getSdkStatus(android.content.Context context, optional String providerPackageName);
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);
- method @Deprecated 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 @Deprecated @androidx.core.os.BuildCompat.PrereleaseSdkCheck public default static boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
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);
- method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
- method public int getSdkStatus(android.content.Context context);
- method public int getSdkStatus(android.content.Context context, optional String providerPackageName);
+ 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);
+ 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 int getSdkStatus(android.content.Context context);
+ method @androidx.core.os.BuildCompat.PrereleaseSdkCheck public int getSdkStatus(android.content.Context context, optional String providerPackageName);
method @Deprecated public boolean isApiSupported();
- method @Deprecated public boolean isProviderAvailable(android.content.Context context);
- method @Deprecated public boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
- property public final String ACTION_HEALTH_CONNECT_SETTINGS;
+ method @Deprecated @androidx.core.os.BuildCompat.PrereleaseSdkCheck public boolean isProviderAvailable(android.content.Context context);
+ method @Deprecated @androidx.core.os.BuildCompat.PrereleaseSdkCheck public boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
+ 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();
- 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 @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 @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 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();
- method 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();
+ 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);
}
}
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 4da244d..b9316fe 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, java.util.List<java.lang.String> recordIdsList, java.util.List<java.lang.String> clientRecordIdsList, 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);
- method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
method public androidx.health.connect.client.PermissionController getPermissionController();
- method public default static int getSdkStatus(android.content.Context context);
- method public default static int getSdkStatus(android.content.Context context, optional String providerPackageName);
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);
- method @Deprecated public default static boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
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);
- method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional String providerPackageName);
- method public int getSdkStatus(android.content.Context context);
- method public int getSdkStatus(android.content.Context context, optional String providerPackageName);
method @Deprecated public boolean isApiSupported();
- method @Deprecated public boolean isProviderAvailable(android.content.Context context);
- method @Deprecated public boolean isProviderAvailable(android.content.Context context, optional String providerPackageName);
- 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();
- 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 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();
- method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract(optional String providerPackageName);
}
}
diff --git a/health/connect/connect-client/build.gradle b/health/connect/connect-client/build.gradle
index f3b4530..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"))
}
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/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..3cccbb7
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
@@ -0,0 +1,545 @@
+/*
+ * 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.WeightRecord
+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)
+ ),
+ WeightRecord(
+ time = Instant.ofEpochMilli(1234L),
+ zoneOffset = ZoneOffset.UTC,
+ weight = Mass.kilograms(100.0)
+ ),
+ )
+ )
+
+ val aggregateResponse =
+ healthConnectClient.aggregate(
+ AggregateRequest(
+ setOf(
+ StepsRecord.COUNT_TOTAL,
+ HeartRateRecord.BPM_MIN,
+ HeartRateRecord.BPM_MAX,
+ NutritionRecord.ENERGY_TOTAL,
+ NutritionRecord.CAFFEINE_TOTAL,
+ WeightRecord.WEIGHT_MAX,
+ 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(this[WeightRecord.WEIGHT_MAX]).isEqualTo(Mass.kilograms(100.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..0e90c45
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RecordConvertersTest.kt
@@ -0,0 +1,1599 @@
+/*
+ * 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,
+ calcium = Mass.grams(15.0),
+ caffeine = Mass.grams(20.0),
+ chloride = Mass.grams(25.0),
+ cholesterol = Mass.grams(30.0),
+ chromium = Mass.grams(35.0),
+ copper = Mass.grams(40.0),
+ molybdenum = Mass.grams(45.0),
+ monounsaturatedFat = Mass.grams(50.0),
+ energy = Energy.calories(300.0)
+ )
+
+ val platformNutrition = nutrition.toPlatformRecord() as PlatformNutritionRecord
+
+ assertPlatformRecord(platformNutrition) {
+ assertThat(calcium!!.inGrams).isWithin(tolerance).of(15.0)
+ assertThat(caffeine!!.inGrams).isWithin(tolerance).of(20.0)
+ assertThat(chloride!!.inGrams).isWithin(tolerance).of(25.0)
+ assertThat(cholesterol!!.inGrams).isWithin(tolerance).of(30.0)
+ assertThat(chromium!!.inGrams).isWithin(tolerance).of(35.0)
+ assertThat(copper!!.inGrams).isWithin(tolerance).of(40.0)
+ assertThat(molybdenum!!.inGrams).isWithin(tolerance).of(45.0)
+ assertThat(monounsaturatedFat!!.inGrams).isWithin(tolerance).of(50.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",
+ stages =
+ listOf(
+ SleepSessionRecord.Stage(
+ START_TIME,
+ START_TIME.plusMillis(40),
+ SleepSessionRecord.STAGE_TYPE_DEEP
+ )
+ )
+ )
+ .toPlatformRecord() as PlatformSleepSessionRecord
+
+ assertPlatformRecord(platformSleepSession) {
+ assertThat(title).isEqualTo("Night night")
+ assertThat(notes).isEqualTo("Many dreams")
+ assertThat(stages)
+ .containsExactly(
+ PlatformSleepSessionStage(
+ START_TIME,
+ START_TIME.plusMillis(40),
+ PlatformSleepStageType.STAGE_TYPE_SLEEPING_DEEP
+ )
+ )
+ }
+ }
+
+ @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, 1.6)
+ .setZoneOffset(ZONE_OFFSET)
+ .build()
+ .toSdkRecord() as HeartRateVariabilityRmssdRecord
+
+ assertSdkRecord(sdkHeartRateVariabilityRmssd) {
+ assertThat(heartRateVariabilityMillis).isEqualTo(1.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)
+ .setStages(
+ listOf(
+ PlatformSleepSessionStage(
+ START_TIME,
+ START_TIME.plusMillis(1),
+ PlatformSleepStageType.STAGE_TYPE_AWAKE
+ ),
+ PlatformSleepSessionStage(
+ END_TIME.minusMillis(1),
+ END_TIME,
+ PlatformSleepStageType.STAGE_TYPE_SLEEPING
+ )
+ )
+ )
+ .build()
+ .toSdkRecord() as SleepSessionRecord
+
+ assertSdkRecord(sdkSleepSession) {
+ assertThat(title).isEqualTo("nap")
+ assertThat(notes).isEqualTo("Afternoon reset")
+ assertThat(stages)
+ .containsExactly(
+ SleepSessionRecord.Stage(
+ START_TIME,
+ START_TIME.plusMillis(1),
+ SleepSessionRecord.STAGE_TYPE_AWAKE
+ ),
+ SleepSessionRecord.Stage(
+ END_TIME.minusMillis(1),
+ END_TIME,
+ SleepSessionRecord.STAGE_TYPE_SLEEPING
+ )
+ )
+ }
+ }
+
+ @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..ddba2fe
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RequestConvertersTest.kt
@@ -0,0 +1,244 @@
+/*
+ * 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(isAscending).isTrue() // Default Order
+ assertThat(dataOrigins)
+ .containsExactly(
+ PlatformDataOrigin.Builder().setPackageName("package1").build(),
+ PlatformDataOrigin.Builder().setPackageName("package2").build()
+ )
+ }
+ }
+
+ @Test
+ fun readRecordsRequest_fromSdkToPlatform_ascendingOrderIgnoredWhenPageTokenIsSet() {
+ val sdkRequest =
+ ReadRecordsRequest(
+ StepsRecord::class,
+ TimeRangeFilter.between(Instant.ofEpochMilli(123L), Instant.ofEpochMilli(456L)),
+ setOf(DataOrigin("package1"), DataOrigin("package2")),
+ ascendingOrder = false,
+ pageToken = "123"
+ )
+
+ with(sdkRequest.toPlatformRequest(SystemDefaultTimeSource)) {
+ assertThat(recordType).isAssignableTo(PlatformStepsRecord::class.java)
+ assertThat(pageToken).isEqualTo(123)
+ 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..42d97d3
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/AggregationMappings.kt
@@ -0,0 +1,189 @@
+/*
+ * 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 GRAMS_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
+ )
+
+internal val KILOGRAMS_AGGREGATION_METRIC_TYPE_MAP:
+ Map<AggregateMetric<Mass>, PlatformAggregateMetric<PlatformMass>> =
+ mapOf(
+ 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..9c12607
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/IntDefMappings.kt
@@ -0,0 +1,489 @@
+/*
+ * 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.SleepSessionRecord
+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.reversed()
+
+internal val SDK_TO_PLATFORM_SLEEP_STAGE_TYPE: Map<Int, Int> =
+ mapOf(
+ SleepSessionRecord.STAGE_TYPE_AWAKE to PlatformSleepStageType.STAGE_TYPE_AWAKE,
+ SleepSessionRecord.STAGE_TYPE_SLEEPING to PlatformSleepStageType.STAGE_TYPE_SLEEPING,
+ SleepSessionRecord.STAGE_TYPE_OUT_OF_BED to
+ PlatformSleepStageType.STAGE_TYPE_AWAKE_OUT_OF_BED,
+ SleepSessionRecord.STAGE_TYPE_LIGHT to PlatformSleepStageType.STAGE_TYPE_SLEEPING_LIGHT,
+ SleepSessionRecord.STAGE_TYPE_DEEP to PlatformSleepStageType.STAGE_TYPE_SLEEPING_DEEP,
+ SleepSessionRecord.STAGE_TYPE_REM to PlatformSleepStageType.STAGE_TYPE_SLEEPING_REM,
+ SleepSessionRecord.STAGE_TYPE_AWAKE_IN_BED to PlatformSleepStageType.STAGE_TYPE_AWAKE_IN_BED
+ )
+
+internal val PLATFORM_TO_SDK_SLEEP_STAGE_TYPE = SDK_TO_PLATFORM_SLEEP_STAGE_TYPE.reversed()
+
+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.toPlatformSleepStageType(): Int {
+ return SDK_TO_PLATFORM_SLEEP_STAGE_TYPE[this] ?: PlatformSleepStageType.STAGE_TYPE_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
+}
+
+internal fun Int.toSdkSleepStageType(): Int {
+ return PLATFORM_TO_SDK_SLEEP_STAGE_TYPE[this] ?: SleepSessionRecord.STAGE_TYPE_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..89ed85e
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/PlatformRecordAliases.kt
@@ -0,0 +1,333 @@
+/*
+ * 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 PlatformSleepSessionStage =
+ android.health.connect.datatypes.SleepSessionRecord.Stage
+
+internal typealias PlatformSleepStageType =
+ android.health.connect.datatypes.SleepSessionRecord.StageType
+
+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..0540f8e
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RecordConverters.kt
@@ -0,0 +1,993 @@
+/*
+ * 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(),
+ stages = stages.map { it.toSdkSleepSessionStage() },
+ )
+
+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()) }
+ caffeine?.let { setCaffeine(it.toPlatformMass()) }
+ calcium?.let { setCalcium(it.toPlatformMass()) }
+ chloride?.let { setChloride(it.toPlatformMass()) }
+ cholesterol?.let { setCholesterol(it.toPlatformMass()) }
+ chromium?.let { setChromium(it.toPlatformMass()) }
+ copper?.let { setCopper(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()) }
+ molybdenum?.let { setMolybdenum(it.toPlatformMass()) }
+ monounsaturatedFat?.let { setMonounsaturatedFat(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) }
+ setStages(stages.map { it.toPlatformSleepSessionStage() })
+ }
+ .build()
+
+private fun SleepSessionRecord.Stage.toPlatformSleepSessionStage() =
+ PlatformSleepSessionStage(startTime, endTime, stage.toPlatformSleepStageType())
+
+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)
+
+private fun PlatformSleepSessionStage.toSdkSleepSessionStage() =
+ SleepSessionRecord.Stage(startTime, endTime, type.toSdkSleepStageType())
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..5c7896d
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RequestConverters.kt
@@ -0,0 +1,141 @@
+/*
+ * 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))
+ .setPageSize(pageSize)
+ .apply {
+ dataOriginFilter.forEach { addDataOrigins(it.toPlatformDataOrigin()) }
+ pageToken?.let { setPageToken(it.toLong()) }
+ // Platform doesn't allow setting both pageToken and ascendingOrder together.
+ if (pageToken == null) {
+ setAscending(ascendingOrder)
+ }
+ }
+ .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>?
+ ?: GRAMS_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType<Any>?
+ ?: KILOGRAMS_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..d1fd401a9
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/ResponseConverters.kt
@@ -0,0 +1,120 @@
+/*
+ * 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 androidx.health.connect.client.units.Mass
+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 GRAMS_AGGREGATION_METRIC_TYPE_MAP -> {
+ this[key.metricKey] = (value as PlatformMass).inGrams
+ }
+ in KILOGRAMS_AGGREGATION_METRIC_TYPE_MAP -> {
+ this[key.metricKey] = Mass.grams((value as PlatformMass).inGrams).inKilograms
+ }
+ 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/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 ce07b53..f6e78d2 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(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ ctor public AvifWriter.Builder(String, @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(java.io.FileDescriptor, int, int, int);
- ctor public HeifWriter.Builder(String, 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(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ ctor public HeifWriter.Builder(String, @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 ce07b53..f6e78d2 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(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ ctor public AvifWriter.Builder(String, @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(java.io.FileDescriptor, int, int, int);
- ctor public HeifWriter.Builder(String, 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(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ ctor public HeifWriter.Builder(String, @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 ce07b53..f6e78d2 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(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ ctor public AvifWriter.Builder(String, @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(java.io.FileDescriptor, int, int, int);
- ctor public HeifWriter.Builder(String, 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(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
+ ctor public HeifWriter.Builder(String, @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/lint-baseline.xml b/heifwriter/heifwriter/lint-baseline.xml
index bcf9c5b..d1c1def 100644
--- a/heifwriter/heifwriter/lint-baseline.xml
+++ b/heifwriter/heifwriter/lint-baseline.xml
@@ -1,49 +1,31 @@
<?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-beta02" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta02)" variant="all" version="8.1.0-beta02">
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1="public class EglRectBlt {"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/EglRectBlt.java"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1="public class EglWindowSurface {"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/EglWindowSurface.java"/>
- </issue>
-
- <issue
- id="BanHideAnnotation"
- message="@hide is not allowed in Javadoc"
- errorLine1="public final class HeifEncoder implements AutoCloseable,"
+ errorLine1="public final class AvifEncoder extends EncoderBase {"
errorLine2=" ~~~~~~~~~~~">
<location
- file="src/main/java/androidx/heifwriter/HeifEncoder.java"/>
+ file="src/main/java/androidx/heifwriter/AvifEncoder.java"/>
</issue>
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1=" public @interface InputMode {}"
+ errorLine1=" public @interface InputMode {"
errorLine2=" ~~~~~~~~~">
<location
- file="src/main/java/androidx/heifwriter/HeifWriter.java"/>
+ file="src/main/java/androidx/heifwriter/AvifWriter.java"/>
</issue>
<issue
id="BanHideAnnotation"
message="@hide is not allowed in Javadoc"
- errorLine1="public class Texture2dProgram {"
- errorLine2=" ~~~~~~~~~~~~~~~~">
+ errorLine1="public class EncoderBase implements AutoCloseable,"
+ errorLine2=" ~~~~~~~~~~~">
<location
- file="src/main/java/androidx/heifwriter/Texture2dProgram.java"/>
+ file="src/main/java/androidx/heifwriter/EncoderBase.java"/>
</issue>
<issue
@@ -56,12 +38,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 +70,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 +79,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 +88,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,232 +97,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=" public static FloatBuffer createFloatBuffer(float[] coords) {"
- errorLine2=" ~~~~~~~~~~~">
+ errorLine1=" protected static String findAv1Fallback() {"
+ errorLine2=" ~~~~~~">
<location
- file="src/main/java/androidx/heifwriter/EglRectBlt.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 FloatBuffer createFloatBuffer(float[] coords) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/EglRectBlt.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 EglRectBlt(Texture2dProgram program, int texWidth, int texHeight) {"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/EglRectBlt.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 loadTexture(int texId, Bitmap bitmap) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/EglRectBlt.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 copyRect(int textureId, float[] texMatrix, Rect texRect) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/EglRectBlt.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 copyRect(int textureId, float[] texMatrix, Rect texRect) {"
- errorLine2=" ~~~~">
- <location
- file="src/main/java/androidx/heifwriter/EglRectBlt.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 EglWindowSurface(Surface surface) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/EglWindowSurface.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 Surface getSurface() {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/EglWindowSurface.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 setRotation(int rotation) {"
- 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"/>
- </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 loadTexture(int texId, Bitmap bitmap) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/Texture2dProgram.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 draw(float[] mvpMatrix, FloatBuffer vertexBuffer, int firstVertex,"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/Texture2dProgram.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 draw(float[] mvpMatrix, FloatBuffer vertexBuffer, int firstVertex,"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/Texture2dProgram.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=" float[] texMatrix, FloatBuffer texBuffer, int textureId, int texStride) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/Texture2dProgram.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=" float[] texMatrix, FloatBuffer texBuffer, int textureId, int texStride) {"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/Texture2dProgram.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 int createProgram(String vertexSource, String fragmentSource) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/Texture2dProgram.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 int createProgram(String vertexSource, String fragmentSource) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/Texture2dProgram.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 int loadShader(int shaderType, String source) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/Texture2dProgram.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 void checkLocation(int location, String label) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/Texture2dProgram.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 void checkGlError(String op) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/heifwriter/Texture2dProgram.java"/>
+ file="src/main/java/androidx/heifwriter/AvifEncoder.java"/>
</issue>
</issues>
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/EglRectBlt.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EglRectBlt.java
index c2489cf..22a1c35 100644
--- a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EglRectBlt.java
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EglRectBlt.java
@@ -26,10 +26,8 @@
/**
* This class represents a viewport-sized sprite that will be rendered with
* a subrect from a texture.
- *
- * @hide
*/
-public class EglRectBlt {
+class EglRectBlt {
private static final int SIZEOF_FLOAT = 4;
/**
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java
index 35d34d4..4aade72 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;
/**
@@ -33,10 +35,8 @@
* The constructor takes a Surface obtained from MediaCodec.createInputSurface(), and uses that
* to create an EGL window surface. Calls to eglSwapBuffers() cause a frame of data to be sent
* to the video encoder.
- *
- * @hide
*/
-public class EglWindowSurface {
+class EglWindowSurface {
private static final String TAG = "EglWindowSurface";
private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY;
@@ -52,18 +52,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 +80,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 +196,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..a0b0e9a 100644
--- a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
@@ -16,163 +16,46 @@
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.
- *
+ * <p>
* It currently supports three input modes: {@link #INPUT_MODE_BUFFER},
* {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
- *
+ * <p>
* 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.
- *
+ * <p>
* 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 HeifEncoder implements AutoCloseable,
- SurfaceTexture.OnFrameAvailableListener {
+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 +72,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 +96,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 +110,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 +123,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 +138,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..a64aa21 100644
--- a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
@@ -32,8 +32,10 @@
import android.view.Surface;
import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
import java.io.FileDescriptor;
import java.io.IOException;
@@ -77,42 +79,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 +102,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 */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@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 +138,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 +154,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 +180,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 +199,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 +211,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 +230,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 +245,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 +258,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 +270,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/Texture2dProgram.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/Texture2dProgram.java
index 6ba6f92..668a660 100644
--- a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/Texture2dProgram.java
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/Texture2dProgram.java
@@ -31,12 +31,10 @@
/**
* GL program and supporting functions for textured 2D shapes.
- *
+ * <p>
* (Contains mostly code borrowed from Grafika)
- *
- * @hide
*/
-public class Texture2dProgram {
+class Texture2dProgram {
private static final String TAG = "Texture2dProgram";
private static final boolean DEBUG = false;
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/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/libraryversions.toml b/libraryversions.toml
index 89f1543..463c6c6 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -29,7 +29,7 @@
CONSTRAINTLAYOUT_CORE = "1.1.0-alpha10"
CONTENTPAGER = "1.1.0-alpha01"
COORDINATORLAYOUT = "1.3.0-alpha01"
-CORE = "1.11.0-alpha05"
+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-beta05"
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/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-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-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-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-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-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/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-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-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-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/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-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-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-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-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/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/media/media/api/current.txt b/media/media/api/current.txt
index 88dc4db3..2509d03 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 88dc4db3..2509d03 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 a40b59b..4f0864c 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 783d543..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)
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/mediarouter/mediarouter/api/current.txt b/mediarouter/mediarouter/api/current.txt
index fbe13f9..36663f8 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 fbe13f9..36663f8 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 fbe13f9..36663f8 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 46de044..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"))
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/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/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/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..8db037e 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,16 +30,17 @@
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
import org.junit.Assert.assertThrows
import org.junit.Assume.assumeTrue
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
@@ -283,7 +288,46 @@
verify(context, Mockito.never()).getSystemService(any())
}
- @Ignore("b/277764220")
+ @Test
+ // TODO(b/249982507) DexmakerMockitoInline requires P+. Rewrite to support P-
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+ 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>()
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/TestActivityHolder.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/TestActivityHolder.kt
new file mode 100644
index 0000000..66a8470
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/TestActivityHolder.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.sdkruntime.client
+
+import android.app.Activity
+import androidx.activity.OnBackPressedDispatcher
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleRegistry
+import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
+
+internal class TestActivityHolder(
+ private val activity: Activity,
+) : ActivityHolder {
+ val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
+ val backPresseddispatcher: OnBackPressedDispatcher = OnBackPressedDispatcher()
+
+ override fun getActivity(): Activity = activity
+
+ override fun getOnBackPressedDispatcher(): OnBackPressedDispatcher = backPresseddispatcher
+
+ override val lifecycle: Lifecycle
+ get() = lifecycleRegistry
+}
\ No newline at end of file
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..e418460
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/activity/LocalSdkActivityStarterTest.kt
@@ -0,0 +1,80 @@
+/*
+ * 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)
+
+ val sdkActivity = activityHolder.getActivity() as SdkActivity
+ assertThat(activityHolder.lifecycle).isSameInstanceAs(sdkActivity.lifecycle)
+ assertThat(activityHolder.getOnBackPressedDispatcher())
+ .isSameInstanceAs(sdkActivity.onBackPressedDispatcher)
+ }
+
+ @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..314b10e 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,11 @@
import android.content.Context
import android.os.Binder
import android.os.Bundle
+import android.os.IBinder
import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.privacysandbox.sdkruntime.client.EmptyActivity
+import androidx.privacysandbox.sdkruntime.client.TestActivityHolder
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 +32,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 +131,75 @@
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 = TestActivityHolder(this)
+ localHandler.onActivityCreated(activityHolder)
+
+ val receivedActivityHolder = catchingHandler.result!!
+ val receivedActivity = receivedActivityHolder.getActivity()
+ assertThat(receivedActivity).isSameInstanceAs(activityHolder.getActivity())
+ }
+ }
+ }
+
+ @Test
+ fun sdkSandboxActivityHandler_ReceivesLifecycleEventsFromOriginalActivityHolder() {
+ 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 = TestActivityHolder(this)
+ localHandler.onActivityCreated(activityHolder)
+ val receivedActivityHolder = catchingHandler.result!!
+
+ for (event in Lifecycle.Event.values().filter { it != Lifecycle.Event.ON_ANY }) {
+ activityHolder.lifecycleRegistry.handleLifecycleEvent(event)
+ assertThat(receivedActivityHolder.getLifeCycleCurrentState())
+ .isEqualTo(event.targetState)
+ }
+ }
+ }
+ }
+
+ @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 +242,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 +297,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 +310,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 +330,7 @@
}
// add SDK loaded from test sources
+ val controller = TestStubController()
add(
arrayOf(
"BuiltFromSource",
@@ -275,26 +362,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 40aee3a..44e1ce4 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,17 @@
package androidx.privacysandbox.sdkruntime.client.loader
+import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.os.IBinder
+import androidx.lifecycle.Lifecycle
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
import androidx.privacysandbox.sdkruntime.core.Versions
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
+import java.lang.reflect.Proxy
+import java.util.concurrent.CountDownLatch
import kotlin.reflect.cast
/**
@@ -72,6 +77,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 +89,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 +179,52 @@
}
/**
+ * 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
+ }
+
+ fun getLifeCycleCurrentState(): Lifecycle.State {
+ val lifecycle = activityHolder.callMethod(
+ methodName = "getLifecycle"
+ )
+ val currentState = lifecycle!!.callMethod(
+ methodName = "getCurrentState"
+ )
+ val currentStateString = currentState!!.callMethod(
+ methodName = "name"
+ ) as String
+ return Lifecycle.State.valueOf(currentStateString)
+ }
+}
+
+/**
* 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..8141f38 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,11 +17,13 @@
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.Versions
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -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..4aad685
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/ActivityHolderProxyFactoryTest.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.activity.OnBackPressedCallback
+import androidx.lifecycle.Lifecycle
+import androidx.privacysandbox.sdkruntime.client.EmptyActivity
+import androidx.privacysandbox.sdkruntime.client.TestActivityHolder
+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 = TestActivityHolder(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 = TestActivityHolder(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_ProxyBackPressedFromSourceDispatcher() {
+ with(ActivityScenario.launch(EmptyActivity::class.java)) {
+ withActivity {
+ val activityHolder = TestActivityHolder(this)
+ val proxy = factory.createProxyFor(activityHolder) as ActivityHolder
+ val callback = CountingOnBackPressedCallback()
+
+ val sourceDispatcher = activityHolder.getOnBackPressedDispatcher()
+ val proxyDispatcher = proxy.getOnBackPressedDispatcher()
+
+ proxyDispatcher.addCallback(callback)
+ sourceDispatcher.onBackPressed()
+ assertThat(callback.count).isEqualTo(1)
+
+ callback.remove()
+ sourceDispatcher.onBackPressed()
+ assertThat(callback.count).isEqualTo(1)
+ }
+ }
+ }
+
+ @Test
+ fun getOnBackPressedDispatcher_EnableSourceCallbackOnlyWhenEnabledCallbackAddedToProxy() {
+ with(ActivityScenario.launch(EmptyActivity::class.java)) {
+ withActivity {
+ val activityHolder = TestActivityHolder(this)
+ val proxy = factory.createProxyFor(activityHolder) as ActivityHolder
+ val callback = CountingOnBackPressedCallback()
+
+ val sourceDispatcher = activityHolder.getOnBackPressedDispatcher()
+ val proxyDispatcher = proxy.getOnBackPressedDispatcher()
+
+ assertThat(sourceDispatcher.hasEnabledCallbacks()).isFalse()
+
+ proxyDispatcher.addCallback(callback)
+ assertThat(sourceDispatcher.hasEnabledCallbacks()).isTrue()
+
+ callback.isEnabled = false
+ assertThat(sourceDispatcher.hasEnabledCallbacks()).isFalse()
+
+ callback.isEnabled = true
+ assertThat(sourceDispatcher.hasEnabledCallbacks()).isTrue()
+
+ callback.remove()
+ assertThat(sourceDispatcher.hasEnabledCallbacks()).isFalse()
+ }
+ }
+ }
+
+ @Test
+ fun getLifecycle_ProxyLifecycleEventsFromSourceActivityHolder() {
+ with(ActivityScenario.launch(EmptyActivity::class.java)) {
+ withActivity {
+ val sourceActivityHolder = TestActivityHolder(this)
+ val proxy = factory.createProxyFor(sourceActivityHolder) as ActivityHolder
+ for (event in Lifecycle.Event.values().filter { it != Lifecycle.Event.ON_ANY }) {
+ sourceActivityHolder.lifecycleRegistry.handleLifecycleEvent(event)
+ assertThat(proxy.lifecycle.currentState).isEqualTo(event.targetState)
+ }
+ }
+ }
+ }
+
+ private class CountingOnBackPressedCallback : OnBackPressedCallback(true) {
+ var count = 0
+
+ override fun handleOnBackPressed() {
+ count++
+ }
+ }
+}
\ 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..c1c3d08 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.OptIn
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresExtension
+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..ed9d37e
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/ActivityHolderProxyFactory.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.privacysandbox.sdkruntime.client.loader.impl.injector
+
+import android.app.Activity
+import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
+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 backPressedDispatcherProxyFactory: BackPressedDispatcherProxyFactory,
+
+ private val lifecycleRegistryProxyFactory: LifecycleRegistryProxyFactory,
+) {
+
+ fun createProxyFor(activityHolder: ActivityHolder): Any {
+ val dispatcherProxy = backPressedDispatcherProxyFactory.setupOnBackPressedDispatcherProxy(
+ activityHolder.getOnBackPressedDispatcher()
+ )
+
+ val handler = ActivityHolderHandler(
+ activityHolder.getActivity(),
+ dispatcherProxy
+ )
+
+ val activityHolderProxy = Proxy.newProxyInstance(
+ sdkClassLoader,
+ arrayOf(activityHolderClass),
+ handler
+ )
+
+ val lifecycleProxy = lifecycleRegistryProxyFactory.setupLifecycleProxy(
+ activityHolderProxy,
+ activityHolder.lifecycle
+ )
+ handler.lifecycleProxy = lifecycleProxy
+
+ return 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 lifecycleRegistryProxyFactory = LifecycleRegistryProxyFactory.createFor(classLoader)
+ val backPressedDispatcherProxyFactory =
+ BackPressedDispatcherProxyFactory.createFor(classLoader)
+
+ return ActivityHolderProxyFactory(
+ sdkClassLoader = classLoader,
+ activityHolderClass = activityHolderClass,
+ backPressedDispatcherProxyFactory = backPressedDispatcherProxyFactory,
+ lifecycleRegistryProxyFactory = lifecycleRegistryProxyFactory
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/BackPressedDispatcherProxyFactory.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/BackPressedDispatcherProxyFactory.kt
new file mode 100644
index 0000000..794d5c7
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/BackPressedDispatcherProxyFactory.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.activity.OnBackPressedCallback
+import androidx.activity.OnBackPressedDispatcher
+import java.lang.reflect.Constructor
+import java.lang.reflect.InvocationHandler
+import java.lang.reflect.Method
+import java.lang.reflect.Proxy
+
+/**
+ * Create instance of [OnBackPressedDispatcher] class loaded by SDK Classloader.
+ * Set callback for [OnBackPressedDispatcher.onHasEnabledCallbacksChanged] and enable/disable
+ * callback in original [OnBackPressedDispatcher].
+ * Proxy [OnBackPressedDispatcher.onBackPressed] from original dispatcher to proxy.
+ */
+internal class BackPressedDispatcherProxyFactory(
+ private val onBackPressedDispatcherConstructor: Constructor<out Any>,
+ private val consumerClass: Class<*>,
+ private val dispatcherOnBackPressedMethod: Method,
+ private val sdkClassLoader: ClassLoader
+) {
+ fun setupOnBackPressedDispatcherProxy(
+ sourceDispatcher: OnBackPressedDispatcher
+ ): Any {
+ val enabledChangedHandler = OnHasEnabledCallbacksChangedHandler()
+
+ val onHasEnabledCallbacksChangedCallback = Proxy.newProxyInstance(
+ sdkClassLoader,
+ arrayOf(consumerClass),
+ enabledChangedHandler
+ )
+ val dispatcherProxy = onBackPressedDispatcherConstructor.newInstance(
+ /* parameter1 */ null,
+ /* parameter2 */ onHasEnabledCallbacksChangedCallback
+ )
+
+ val sourceDispatcherCallback =
+ SourceDispatcherCallback(dispatcherProxy, dispatcherOnBackPressedMethod)
+
+ enabledChangedHandler.sourceDispatcherCallback = sourceDispatcherCallback
+
+ sourceDispatcher.addCallback(sourceDispatcherCallback)
+
+ return dispatcherProxy
+ }
+
+ private class OnHasEnabledCallbacksChangedHandler : InvocationHandler {
+
+ var sourceDispatcherCallback: OnBackPressedCallback? = 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()
+ "accept" -> sourceDispatcherCallback?.isEnabled = args!![0] as Boolean
+ else -> {
+ throw UnsupportedOperationException(
+ "Unexpected method call object:$proxy, method: $method, args: $args"
+ )
+ }
+ }
+ }
+ }
+
+ private class SourceDispatcherCallback(
+ private val dispatcherProxy: Any,
+ private val dispatcherOnBackPressedMethod: Method,
+ ) : OnBackPressedCallback(false) {
+
+ @SuppressLint("BanUncheckedReflection") // using reflection on library classes
+ override fun handleOnBackPressed() {
+ dispatcherOnBackPressedMethod.invoke(dispatcherProxy)
+ }
+ }
+
+ companion object {
+ fun createFor(classLoader: ClassLoader): BackPressedDispatcherProxyFactory {
+ val onBackPressedDispatcherClass = Class.forName(
+ "androidx.activity.OnBackPressedDispatcher",
+ /* initialize = */ false,
+ classLoader
+ )
+
+ val consumerClass = Class.forName(
+ "androidx.core.util.Consumer",
+ /* initialize = */ false,
+ classLoader
+ )
+
+ val onBackPressedDispatcherConstructor = onBackPressedDispatcherClass.getConstructor(
+ /* parameter1 */ Runnable::class.java,
+ /* parameter2 */ consumerClass
+ )
+
+ val dispatcherOnBackPressedMethod = onBackPressedDispatcherClass
+ .getMethod("onBackPressed")
+
+ return BackPressedDispatcherProxyFactory(
+ onBackPressedDispatcherConstructor,
+ consumerClass,
+ dispatcherOnBackPressedMethod,
+ classLoader
+ )
+ }
+ }
+}
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/LifecycleRegistryProxyFactory.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/LifecycleRegistryProxyFactory.kt
new file mode 100644
index 0000000..509cf50
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/injector/LifecycleRegistryProxyFactory.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.privacysandbox.sdkruntime.client.loader.impl.injector
+
+import android.annotation.SuppressLint
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
+import java.lang.reflect.Constructor
+import java.lang.reflect.Method
+
+/**
+ * Create proxy of [Lifecycle] that implements same interface loaded by SDK Classloader.
+ * Proxy [Lifecycle.Event] from original object to proxy.
+ */
+internal class LifecycleRegistryProxyFactory private constructor(
+ private val lifecycleRegistryConstructor: Constructor<out Any>,
+ private val lifecycleEventInstances: Map<String, Any>,
+ private val handleLifecycleEventMethod: Method
+) {
+ fun setupLifecycleProxy(
+ activityHolderProxy: Any,
+ sourceLifecycle: Lifecycle
+ ): Any {
+ val registry = lifecycleRegistryConstructor.newInstance(activityHolderProxy)
+ sourceLifecycle.addObserver(object : LifecycleEventObserver {
+ @SuppressLint("BanUncheckedReflection") // using reflection on library classes
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ val enumInstance = lifecycleEventInstances[event.name]
+ if (enumInstance != null) {
+ handleLifecycleEventMethod.invoke(registry, enumInstance)
+ }
+ }
+ })
+ return registry
+ }
+
+ companion object {
+ fun createFor(classLoader: ClassLoader): LifecycleRegistryProxyFactory {
+ val lifecycleOwnerClass = Class.forName(
+ "androidx.lifecycle.LifecycleOwner",
+ /* initialize = */ false,
+ classLoader
+ )
+ val lifecycleRegistryClass = Class.forName(
+ "androidx.lifecycle.LifecycleRegistry",
+ /* initialize = */ false,
+ classLoader
+ )
+ val lifecycleRegistryConstructor =
+ lifecycleRegistryClass.getConstructor(
+ /* parameter1 */ lifecycleOwnerClass
+ )
+
+ val lifecycleEventEnum =
+ Class.forName(
+ Lifecycle.Event::class.java.name,
+ /* initialize = */ false,
+ classLoader
+ )
+ val lifecycleEventInstances = lifecycleEventEnum
+ .enumConstants
+ .filterIsInstance(Enum::class.java)
+ .associateBy({ it.name }, { it })
+
+ val handleLifecycleEventMethod = lifecycleRegistryClass.getMethod(
+ "handleLifecycleEvent",
+ /* parameter1 */ lifecycleEventEnum
+ )
+
+ return LifecycleRegistryProxyFactory(
+ lifecycleRegistryConstructor,
+ lifecycleEventInstances,
+ handleLifecycleEventMethod
+ )
+ }
+ }
+}
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..fcd99c0 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.Versions
+import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
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..803bb3a 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.OptIn
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+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.Versions
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
+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..fcae673
--- /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.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleRegistry
+import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
+import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
+
+/**
+ * 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 ae23335..bc9a40d 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 ae23335..bc9a40d 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 ae23335..bc9a40d 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/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 8c7e013..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")
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/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/samples/MediaRoutingDemo/build.gradle b/samples/MediaRoutingDemo/build.gradle
index abce76a..456800f 100644
--- a/samples/MediaRoutingDemo/build.gradle
+++ b/samples/MediaRoutingDemo/build.gradle
@@ -8,6 +8,7 @@
implementation(project(":mediarouter:mediarouter"))
implementation(project(":recyclerview:recyclerview"))
implementation(project(":concurrent:concurrent-futures"))
+ implementation(project(":swiperefreshlayout:swiperefreshlayout"))
implementation(libs.material)
implementation(libs.multidex)
diff --git a/samples/MediaRoutingDemo/src/main/AndroidManifest.xml b/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
index b56a49f..bb037aa 100644
--- a/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
+++ b/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
@@ -22,6 +22,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+ <uses-permission android:name="android.permission.BLUETOOTH"
+ android:maxSdkVersion="30" />
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+
<application
android:hardwareAccelerated="true"
android:icon="@drawable/app_sample_code"
@@ -37,6 +41,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 +69,20 @@
</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>
+
+ <activity android:name=".activities.systemrouting.SystemRoutingActivity"
+ android:exported="false" />
+
<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..2db33b8 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,12 +26,14 @@
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
+import android.widget.Button;
import android.widget.Spinner;
import android.widget.Switch;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.AppCompatButton;
import androidx.mediarouter.media.MediaRouter;
import androidx.mediarouter.media.MediaRouterParams;
import androidx.recyclerview.widget.LinearLayoutManager;
@@ -39,6 +41,7 @@
import com.example.androidx.mediarouting.R;
import com.example.androidx.mediarouting.RoutesManager;
+import com.example.androidx.mediarouting.activities.systemrouting.SystemRoutingActivity;
import com.example.androidx.mediarouting.services.SampleDynamicGroupMediaRouteProviderService;
import com.example.androidx.mediarouting.ui.RoutesAdapter;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
@@ -91,6 +94,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);
@@ -124,6 +134,7 @@
setUpTransferToLocalSwitch();
setUpDialogTypeDropDownList();
setUpNewRouteButton();
+ setupSystemRoutesButton();
}
private void setUpDynamicGroupsEnabledSwitch() {
@@ -188,6 +199,11 @@
});
}
+ private void setupSystemRoutesButton() {
+ AppCompatButton showSystemRoutesButton = findViewById(R.id.open_system_routes);
+ showSystemRoutesButton.setOnClickListener(v -> SystemRoutingActivity.launch(this));
+ }
+
private class ProviderServiceConnection implements ServiceConnection {
@Override
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteItem.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteItem.java
new file mode 100644
index 0000000..2bffdee
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteItem.java
@@ -0,0 +1,200 @@
+/*
+ * 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.systemrouting;
+
+import android.text.TextUtils;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * An abstract model that holds information about routes from different sources.
+ *
+ * Can represent media routers' routes, bluetooth routes, or audio routes.
+ */
+public final class SystemRouteItem {
+
+ public static final int ROUTE_SOURCE_MEDIA_ROUTER = 0;
+ public static final int ROUTE_SOURCE_MEDIA_ROUTER2 = 1;
+ public static final int ROUTE_SOURCE_ANDROIDX_ROUTER = 2;
+ public static final int ROUTE_SOURCE_BLUETOOTH_MANAGER = 3;
+ public static final int ROUTE_SOURCE_AUDIO_MANAGER = 4;
+
+ @IntDef({
+ ROUTE_SOURCE_MEDIA_ROUTER,
+ ROUTE_SOURCE_MEDIA_ROUTER2,
+ ROUTE_SOURCE_ANDROIDX_ROUTER,
+ ROUTE_SOURCE_BLUETOOTH_MANAGER,
+ ROUTE_SOURCE_AUDIO_MANAGER
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Type {
+ }
+
+ @NonNull
+ private final String mId;
+
+ @Type
+ private final int mType;
+
+ @NonNull
+ private final String mName;
+
+ @Nullable
+ private final String mAddress;
+
+ @Nullable
+ private final String mDescription;
+
+ private SystemRouteItem(@NonNull Builder builder) {
+ Objects.requireNonNull(builder.mId);
+ Objects.requireNonNull(builder.mName);
+
+ mId = builder.mId;
+ mName = builder.mName;
+ mType = builder.mType;
+
+ mAddress = builder.mAddress;
+ mDescription = builder.mDescription;
+ }
+
+ /**
+ * Returns a unique identifier of a route.
+ */
+ @NonNull
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Returns a route source.
+ *
+ * see {@link SystemRouteItem.Type}
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Returns a human-readable name of the route.
+ */
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns address if the route is a Bluetooth route and {@code null} otherwise.
+ */
+ @Nullable
+ public String getAddress() {
+ return mAddress;
+ }
+
+ /**
+ * Returns a route description or {@code null} if empty.
+ */
+ @Nullable
+ public String getDescription() {
+ return mDescription;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SystemRouteItem that = (SystemRouteItem) o;
+ return mType == that.mType && mId.equals(that.mId) && mName.equals(that.mName)
+ && Objects.equals(mAddress, that.mAddress) && Objects.equals(
+ mDescription, that.mDescription);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mId, mType, mName, mAddress, mDescription);
+ }
+
+ /**
+ * Helps to construct {@link SystemRouteItem}.
+ */
+ public static final class Builder {
+
+ @NonNull
+ private final String mId;
+ @Type
+ private final int mType;
+
+ @NonNull
+ private String mName;
+
+ @Nullable
+ private String mAddress;
+
+ @Nullable
+ private String mDescription;
+
+ public Builder(@NonNull String id, @Type int type) {
+ mId = id;
+ mType = type;
+ }
+
+ /**
+ * Sets a route name.
+ */
+ @NonNull
+ public Builder setName(@NonNull String name) {
+ mName = name;
+ return this;
+ }
+
+ /**
+ * Sets an address for the route.
+ */
+ @NonNull
+ public Builder setAddress(@NonNull String address) {
+ if (!TextUtils.isEmpty(address)) {
+ mAddress = address;
+ }
+ return this;
+ }
+
+ /**
+ * Sets a description for the route.
+ */
+ @NonNull
+ public Builder setDescription(@NonNull String description) {
+ if (!TextUtils.isEmpty(description)) {
+ mDescription = description;
+ }
+ return this;
+ }
+
+ /**
+ * Builds {@link SystemRouteItem}.
+ */
+ @NonNull
+ public SystemRouteItem build() {
+ return new SystemRouteItem(this);
+ }
+ }
+}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteUtils.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteUtils.java
new file mode 100644
index 0000000..c6206f7
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteUtils.java
@@ -0,0 +1,62 @@
+/*
+ * 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.systemrouting;
+
+import android.media.MediaRouter;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * Utils for {@link SystemRouteItem}.
+ */
+public final class SystemRouteUtils {
+
+ private SystemRouteUtils() {
+ // Private on purpose.
+ }
+
+ /**
+ * Checks whether {@link MediaRouter.RouteInfo} is a system route or not.
+ */
+ @RequiresApi(16)
+ public static boolean isSystemMediaRouterRoute(@NonNull MediaRouter.RouteInfo routeInfo) {
+ return (routeInfo.getClass() == MediaRouter.RouteInfo.class);
+ }
+
+ /**
+ * Converts {@link SystemRouteItem.Type} to a human-readable string.
+ */
+ @NonNull
+ public static String getDescriptionForSource(@SystemRouteItem.Type int type) {
+ switch (type) {
+ case SystemRouteItem.ROUTE_SOURCE_MEDIA_ROUTER:
+ return "Media Router";
+ case SystemRouteItem.ROUTE_SOURCE_MEDIA_ROUTER2:
+ return "Media Router 2";
+ case SystemRouteItem.ROUTE_SOURCE_BLUETOOTH_MANAGER:
+ return "Bluetooth Manager";
+ case SystemRouteItem.ROUTE_SOURCE_ANDROIDX_ROUTER:
+ return "Androidx Router";
+ case SystemRouteItem.ROUTE_SOURCE_AUDIO_MANAGER:
+ return "Audio Manager";
+ default:
+ throw new IllegalArgumentException("Unknown system route type: " + type);
+ }
+ }
+
+}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutesAdapter.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutesAdapter.java
new file mode 100644
index 0000000..543dcd8
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutesAdapter.java
@@ -0,0 +1,197 @@
+/*
+ * 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.systemrouting;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.recyclerview.widget.AsyncListDiffer;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.example.androidx.mediarouting.R;
+
+import java.util.List;
+
+class SystemRoutesAdapter extends RecyclerView.Adapter<SystemRoutesAdapter.ViewHolder> {
+
+ private final AsyncListDiffer<SystemRouteItem> mListDiffer =
+ new AsyncListDiffer<>(this, new ItemCallback());
+
+ public void setItems(@NonNull List<SystemRouteItem> newItems) {
+ mListDiffer.submitList(newItems);
+ }
+
+ @NonNull
+ public List<SystemRouteItem> getItems() {
+ return mListDiffer.getCurrentList();
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ Context context = parent.getContext();
+ View view = LayoutInflater.from(context).inflate(R.layout.item_system_route, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ SystemRouteItem route = getItems().get(position);
+
+ holder.mRouteSourceTextView.setText(
+ SystemRouteUtils.getDescriptionForSource(route.getType()));
+ holder.mRouteNameTextView.setText(route.getName());
+ holder.mRouteIdTextView.setText(route.getId());
+
+ showViewIfNotNull(holder.mRouteAddressTextView, route.getAddress());
+ holder.mRouteAddressTextView.setText(route.getAddress());
+
+ showViewIfNotNull(holder.mRouteDescriptionTextView, route.getDescription());
+ holder.mRouteDescriptionTextView.setText(route.getDescription());
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position,
+ @NonNull List<Object> payloads) {
+ if (payloads.isEmpty()) {
+ onBindViewHolder(holder, position);
+ return;
+ }
+
+ for (Object rawPayload : payloads) {
+ if (!(rawPayload instanceof Payload)) {
+ continue;
+ }
+
+ Payload payload = (Payload) rawPayload;
+
+ if (payload.mName != null) {
+ holder.mRouteNameTextView.setText(payload.mName);
+ }
+
+ showViewIfNotNull(holder.mRouteAddressTextView, payload.mAddress);
+ if (payload.mAddress != null) {
+ holder.mRouteAddressTextView.setText(payload.mAddress);
+ }
+
+ showViewIfNotNull(holder.mRouteDescriptionTextView, payload.mDescription);
+ if (payload.mDescription != null) {
+ holder.mRouteDescriptionTextView.setText(payload.mDescription);
+ }
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return getItems().size();
+ }
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+
+ final AppCompatTextView mRouteSourceTextView;
+ final AppCompatTextView mRouteNameTextView;
+ final AppCompatTextView mRouteIdTextView;
+ final AppCompatTextView mRouteAddressTextView;
+ final AppCompatTextView mRouteDescriptionTextView;
+
+ ViewHolder(@NonNull View itemView) {
+ super(itemView);
+
+ mRouteSourceTextView = itemView.findViewById(R.id.route_source);
+ mRouteNameTextView = itemView.findViewById(R.id.route_name);
+ mRouteIdTextView = itemView.findViewById(R.id.route_id);
+ mRouteAddressTextView = itemView.findViewById(R.id.route_address);
+ mRouteDescriptionTextView = itemView.findViewById(R.id.route_description);
+ }
+ }
+
+ private static class ItemCallback extends DiffUtil.ItemCallback<SystemRouteItem> {
+
+ @Override
+ public boolean areItemsTheSame(@NonNull SystemRouteItem oldItem,
+ @NonNull SystemRouteItem newItem) {
+ return oldItem.getId().equals(newItem.getId())
+ && oldItem.getType() == newItem.getType();
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull SystemRouteItem oldItem,
+ @NonNull SystemRouteItem newItem) {
+ return oldItem.equals(newItem);
+ }
+
+ @Nullable
+ @Override
+ public Payload getChangePayload(@NonNull SystemRouteItem oldItem,
+ @NonNull SystemRouteItem newItem) {
+ return new Payload(takeIfChanged(oldItem.getName(), newItem.getName()),
+ takeIfChanged(oldItem.getAddress(), newItem.getAddress()),
+ takeIfChanged(oldItem.getDescription(), newItem.getDescription()));
+ }
+ }
+
+ private static class Payload {
+
+ @Nullable
+ final String mName;
+
+ @Nullable
+ final String mAddress;
+
+ @Nullable
+ final String mDescription;
+
+
+ Payload(@Nullable String name, @Nullable String address,
+ @Nullable String description) {
+ mName = name;
+ mAddress = address;
+ mDescription = description;
+ }
+ }
+
+ private static <T, V extends View> void showViewIfNotNull(@NonNull V view, @Nullable T obj) {
+ if (obj == null) {
+ view.setVisibility(View.GONE);
+ } else {
+ view.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Nullable
+ private static <T> T takeIfChanged(@Nullable T oldObj, @Nullable T newObj) {
+ if (oldObj == null && newObj == null) {
+ return null;
+ }
+
+ if (oldObj == null || newObj == null) {
+ return newObj;
+ }
+
+ if (oldObj.equals(newObj)) {
+ return null;
+ }
+
+ return newObj;
+ }
+}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutingActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutingActivity.java
new file mode 100644
index 0000000..e3020a5
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutingActivity.java
@@ -0,0 +1,124 @@
+/*
+ * 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.systemrouting;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
+import com.example.androidx.mediarouting.R;
+import com.example.androidx.mediarouting.activities.systemrouting.source.ComposedSystemRoutesSource;
+import com.example.androidx.mediarouting.activities.systemrouting.source.SystemRoutesSource;
+
+import java.util.List;
+
+/**
+ * Shows available system routes gathered from different sources.
+ */
+public final class SystemRoutingActivity extends AppCompatActivity {
+
+ private static final int REQUEST_CODE_BLUETOOTH_CONNECT = 4199;
+
+ private SwipeRefreshLayout mSwipeRefreshLayout;
+ private RecyclerView mRecyclerView;
+ private SystemRoutesAdapter mSystemRoutesAdapter = new SystemRoutesAdapter();
+
+ private SystemRoutesSource mSystemRoutesSource;
+
+ /**
+ * Creates and launches an intent to start current activity.
+ */
+ public static void launch(@NonNull Context context) {
+ Intent intent = new Intent(context, SystemRoutingActivity.class);
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_system_routing);
+
+ mSystemRoutesSource = ComposedSystemRoutesSource.create(this);
+
+ mRecyclerView = findViewById(R.id.recycler_view);
+ mSwipeRefreshLayout = findViewById(R.id.pull_to_refresh_layout);
+
+ mRecyclerView.setAdapter(mSystemRoutesAdapter);
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
+
+ mSwipeRefreshLayout.setOnRefreshListener(() -> {
+ refreshSystemRoutesList();
+ mSwipeRefreshLayout.setRefreshing(false);
+ });
+
+ refreshSystemRoutesList();
+ requestBluetoothPermission();
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ if (requestCode == REQUEST_CODE_BLUETOOTH_CONNECT
+ && grantResults.length > 0) {
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ onBluetoothPermissionGranted();
+ } else {
+ onBluetoothPermissionDenied();
+ }
+ }
+ }
+
+ private void refreshSystemRoutesList() {
+ List<SystemRouteItem> systemRoutes = mSystemRoutesSource.fetchRoutes();
+ mSystemRoutesAdapter.setItems(systemRoutes);
+ }
+
+ private void requestBluetoothPermission() {
+ if (ContextCompat.checkSelfPermission(
+ this, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
+ return;
+ }
+
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.BLUETOOTH_CONNECT},
+ REQUEST_CODE_BLUETOOTH_CONNECT);
+ }
+
+ private void onBluetoothPermissionGranted() {
+ mSystemRoutesSource = ComposedSystemRoutesSource.create(this);
+ refreshSystemRoutesList();
+ }
+
+ private void onBluetoothPermissionDenied() {
+ Toast.makeText(this, getString(R.string.system_routing_activity_bluetooth_denied),
+ Toast.LENGTH_LONG).show();
+ }
+}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AndroidXMediaRouterSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AndroidXMediaRouterSystemRoutesSource.java
new file mode 100644
index 0000000..9532b1a
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AndroidXMediaRouterSystemRoutesSource.java
@@ -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 com.example.androidx.mediarouting.activities.systemrouting.source;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.mediarouter.media.MediaRouter;
+
+import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class AndroidXMediaRouterSystemRoutesSource implements SystemRoutesSource {
+
+ @NonNull
+ private final MediaRouter mMediaRouter;
+
+ @NonNull
+ static AndroidXMediaRouterSystemRoutesSource create(@NonNull Context context) {
+ MediaRouter mediaRouter = MediaRouter.getInstance(context);
+ return new AndroidXMediaRouterSystemRoutesSource(mediaRouter);
+ }
+
+ AndroidXMediaRouterSystemRoutesSource(@NonNull MediaRouter mediaRouter) {
+ mMediaRouter = mediaRouter;
+ }
+
+ @NonNull
+ @Override
+ public List<SystemRouteItem> fetchRoutes() {
+ List<SystemRouteItem> out = new ArrayList<>();
+
+ for (MediaRouter.RouteInfo routeInfo : mMediaRouter.getRoutes()) {
+ if (!routeInfo.isDefaultOrBluetooth()) {
+ continue;
+ }
+
+ SystemRouteItem.Builder builder = new SystemRouteItem.Builder(routeInfo.getId(),
+ SystemRouteItem.ROUTE_SOURCE_ANDROIDX_ROUTER)
+ .setName(routeInfo.getName());
+
+ String description = routeInfo.getDescription();
+ if (description != null) {
+ builder.setDescription(description);
+ }
+
+ out.add(builder.build());
+ }
+
+ return out;
+ }
+}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AudioManagerSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AudioManagerSystemRoutesSource.java
new file mode 100644
index 0000000..f3c06d6
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AudioManagerSystemRoutesSource.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 com.example.androidx.mediarouting.activities.systemrouting.source;
+
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.os.Build;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresApi(Build.VERSION_CODES.M)
+class AudioManagerSystemRoutesSource implements SystemRoutesSource {
+
+ @NonNull
+ private final AudioManager mAudioManager;
+
+ static AudioManagerSystemRoutesSource create(@NonNull Context context) {
+ AudioManager audioManager = context.getSystemService(AudioManager.class);
+ return new AudioManagerSystemRoutesSource(audioManager);
+ }
+
+ AudioManagerSystemRoutesSource(@NonNull AudioManager audioManager) {
+ mAudioManager = audioManager;
+ }
+
+ @NonNull
+ @Override
+ public List<SystemRouteItem> fetchRoutes() {
+ List<SystemRouteItem> out = new ArrayList<>();
+
+ for (AudioDeviceInfo audioDeviceInfo :
+ mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
+ SystemRouteItem.Builder builder = new SystemRouteItem.Builder(
+ String.valueOf(audioDeviceInfo.getId()),
+ SystemRouteItem.ROUTE_SOURCE_AUDIO_MANAGER)
+ .setName(audioDeviceInfo.getProductName().toString());
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ builder.setAddress(Api28Impl.getAddress(audioDeviceInfo));
+ }
+
+ out.add(builder.build());
+ }
+
+ return out;
+ }
+
+ @RequiresApi(Build.VERSION_CODES.P)
+ static class Api28Impl {
+ private Api28Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static String getAddress(AudioDeviceInfo audioDeviceInfo) {
+ return audioDeviceInfo.getAddress();
+ }
+
+ }
+}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/BluetoothManagerSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/BluetoothManagerSystemRoutesSource.java
new file mode 100644
index 0000000..965a6b4
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/BluetoothManagerSystemRoutesSource.java
@@ -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 com.example.androidx.mediarouting.activities.systemrouting.source;
+
+import android.Manifest;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RequiresPermission;
+
+import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+class BluetoothManagerSystemRoutesSource implements SystemRoutesSource {
+
+ @NonNull
+ private final BluetoothManager mBluetoothManager;
+ @NonNull
+ private final BluetoothAdapter mBluetoothAdapter;
+
+ @NonNull
+ static BluetoothManagerSystemRoutesSource create(@NonNull Context context) {
+ BluetoothManager bluetoothManager =
+ (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
+ return new BluetoothManagerSystemRoutesSource(bluetoothManager);
+ }
+
+ BluetoothManagerSystemRoutesSource(@NonNull BluetoothManager bluetoothManager) {
+ mBluetoothManager = bluetoothManager;
+ mBluetoothAdapter = mBluetoothManager.getAdapter();
+ }
+
+ @NonNull
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ @Override
+ public List<SystemRouteItem> fetchRoutes() {
+ List<SystemRouteItem> out = new ArrayList<>();
+
+ for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) {
+ out.add(new SystemRouteItem.Builder(/* id= */ device.getAddress(),
+ /* type= */ SystemRouteItem.ROUTE_SOURCE_BLUETOOTH_MANAGER)
+ .setName(device.getName())
+ .setAddress(device.getAddress())
+ .build());
+ }
+
+ return out;
+ }
+}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/ComposedSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/ComposedSystemRoutesSource.java
new file mode 100644
index 0000000..dcac2b5
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/ComposedSystemRoutesSource.java
@@ -0,0 +1,86 @@
+/*
+ * 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.systemrouting.source;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+
+import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Unified source for different System Routes providers.
+ */
+public class ComposedSystemRoutesSource implements SystemRoutesSource {
+
+ private final List<SystemRoutesSource> mSystemRoutesSources;
+
+ /**
+ * Creates an instance of {@link ComposedSystemRoutesSource}.
+ */
+ @NonNull
+ public static ComposedSystemRoutesSource create(@NonNull Context context) {
+ List<SystemRoutesSource> systemRoutesSources = new ArrayList<>();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ systemRoutesSources.add(MediaRouterSystemRoutesSource.create(context));
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ systemRoutesSources.add(MediaRouter2SystemRoutesSource.create(context));
+ }
+
+ systemRoutesSources.add(AndroidXMediaRouterSystemRoutesSource.create(context));
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2
+ && ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT)
+ == PackageManager.PERMISSION_GRANTED) {
+ systemRoutesSources.add(BluetoothManagerSystemRoutesSource.create(context));
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ systemRoutesSources.add(AudioManagerSystemRoutesSource.create(context));
+ }
+
+ return new ComposedSystemRoutesSource(systemRoutesSources);
+ }
+
+ ComposedSystemRoutesSource(@NonNull List<SystemRoutesSource> routesSources) {
+ mSystemRoutesSources = Collections.unmodifiableList(routesSources);
+ }
+
+
+ @NonNull
+ @Override
+ public List<SystemRouteItem> fetchRoutes() {
+ List<SystemRouteItem> out = new ArrayList<>();
+
+ for (SystemRoutesSource source : mSystemRoutesSources) {
+ out.addAll(source.fetchRoutes());
+ }
+
+ return out;
+ }
+}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouter2SystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouter2SystemRoutesSource.java
new file mode 100644
index 0000000..8c8f056
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouter2SystemRoutesSource.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 com.example.androidx.mediarouting.activities.systemrouting.source;
+
+import android.content.Context;
+import android.media.MediaRoute2Info;
+import android.media.MediaRouter2;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresApi(Build.VERSION_CODES.R)
+class MediaRouter2SystemRoutesSource implements SystemRoutesSource {
+
+ @NonNull
+ private MediaRouter2 mMediaRouter2;
+
+ @NonNull
+ static MediaRouter2SystemRoutesSource create(@NonNull Context context) {
+ MediaRouter2 mediaRouter2 = MediaRouter2.getInstance(context);
+ return new MediaRouter2SystemRoutesSource(mediaRouter2);
+ }
+
+ MediaRouter2SystemRoutesSource(@NonNull MediaRouter2 mediaRouter2) {
+ mMediaRouter2 = mediaRouter2;
+ }
+
+ @NonNull
+ @Override
+ public List<SystemRouteItem> fetchRoutes() {
+ List<SystemRouteItem> out = new ArrayList<>();
+
+ for (MediaRoute2Info routeInfo : mMediaRouter2.getRoutes()) {
+ if (!routeInfo.isSystemRoute()) {
+ continue;
+ }
+
+ out.add(new SystemRouteItem.Builder(routeInfo.getId(),
+ SystemRouteItem.ROUTE_SOURCE_MEDIA_ROUTER2)
+ .setName(String.valueOf(routeInfo.getName()))
+ .setDescription(String.valueOf(routeInfo.getDescription()))
+ .build());
+ }
+
+ return out;
+ }
+}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouterSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouterSystemRoutesSource.java
new file mode 100644
index 0000000..b6d8c26
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouterSystemRoutesSource.java
@@ -0,0 +1,97 @@
+/*
+ * 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.systemrouting.source;
+
+import android.content.Context;
+import android.media.MediaRouter;
+import android.os.Build;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem;
+import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
+class MediaRouterSystemRoutesSource implements SystemRoutesSource {
+
+ @NonNull
+ private final MediaRouter mMediaRouter;
+
+ @NonNull
+ static MediaRouterSystemRoutesSource create(@NonNull Context context) {
+ MediaRouter mediaRouter =
+ (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
+ return new MediaRouterSystemRoutesSource(mediaRouter);
+ }
+
+ MediaRouterSystemRoutesSource(@NonNull MediaRouter mediaRouter) {
+ mMediaRouter = mediaRouter;
+ }
+
+ @NonNull
+ @Override
+ public List<SystemRouteItem> fetchRoutes() {
+ int count = mMediaRouter.getRouteCount();
+
+ List<SystemRouteItem> out = new ArrayList<>();
+
+ for (int i = 0; i < count; i++) {
+ MediaRouter.RouteInfo info = mMediaRouter.getRouteAt(i);
+
+ if (!SystemRouteUtils.isSystemMediaRouterRoute(info)) {
+ continue;
+ }
+
+ SystemRouteItem.Builder builder =
+ new SystemRouteItem.Builder(info.getName().toString() /* id */,
+ SystemRouteItem.ROUTE_SOURCE_MEDIA_ROUTER)
+ .setName(info.getName().toString());
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ CharSequence description = Api18Impl.getDescription(info);
+
+ if (description != null) {
+ builder.setDescription(String.valueOf(description));
+ }
+ }
+
+ out.add(builder.build());
+ }
+
+ return out;
+ }
+
+ @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+ static class Api18Impl {
+ private Api18Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ @Nullable
+ static CharSequence getDescription(MediaRouter.RouteInfo routeInfo) {
+ return routeInfo.getDescription();
+ }
+
+ }
+}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/SystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/SystemRoutesSource.java
new file mode 100644
index 0000000..f58f724
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/SystemRoutesSource.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 com.example.androidx.mediarouting.activities.systemrouting.source;
+
+import androidx.annotation.NonNull;
+
+import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem;
+
+import java.util.List;
+
+/**
+ * Abstracts different route sources.
+ */
+public interface SystemRoutesSource {
+
+ /**
+ * Fetches system routes and returns a list of {@link SystemRouteItem}.
+ */
+ @NonNull
+ List<SystemRouteItem> fetchRoutes();
+
+}
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/drawable/rounded_rectangle_gray.xml b/samples/MediaRoutingDemo/src/main/res/drawable/rounded_rectangle_gray.xml
new file mode 100644
index 0000000..dd06b74
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/res/drawable/rounded_rectangle_gray.xml
@@ -0,0 +1,24 @@
+<?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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+
+ <solid android:color="@android:color/darker_gray" />
+ <corners android:radius="16dp" />
+
+</shape>
\ No newline at end of file
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..2572fc6 100644
--- a/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
+++ b/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
@@ -23,6 +23,18 @@
android:layout_height="wrap_content"
android:orientation="vertical">
+ <androidx.appcompat.widget.AppCompatButton
+ android:id="@+id/open_system_routes"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/settings_activity_show_system_routes" />
+
+ <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/activity_system_routing.xml b/samples/MediaRoutingDemo/src/main/res/layout/activity_system_routing.xml
new file mode 100644
index 0000000..c3ef725
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/res/layout/activity_system_routing.xml
@@ -0,0 +1,27 @@
+<?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.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pull_to_refresh_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
\ No newline at end of file
diff --git a/samples/MediaRoutingDemo/src/main/res/layout/item_system_route.xml b/samples/MediaRoutingDemo/src/main/res/layout/item_system_route.xml
new file mode 100644
index 0000000..3034f1d
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/res/layout/item_system_route.xml
@@ -0,0 +1,103 @@
+<?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.
+ -->
+
+<FrameLayout 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="wrap_content">
+
+ <androidx.cardview.widget.CardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:layout_marginTop="8dp"
+ app:cardCornerRadius="0dp">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="8dp">
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/route_source"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentEnd="true"
+ android:textSize="12sp"
+ android:paddingTop="2dp"
+ android:paddingBottom="2dp"
+ android:paddingLeft="6dp"
+ android:paddingRight="6dp"
+ android:background="@drawable/rounded_rectangle_gray"
+ android:textColor="@android:color/white"
+ tools:text="Media Router 1"/>
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/route_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:textSize="18sp"
+ android:textStyle="bold"
+ tools:text="Route #1" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/route_id"
+ android:layout_marginTop="4dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/route_name"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textSize="12sp"
+ android:textStyle="italic"
+ tools:text="id12345678" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/route_address"
+ android:layout_marginTop="16dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/route_id"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:textSize="14sp"
+ tools:text="Address: 12:13:14:15:16" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/route_description"
+ android:layout_marginTop="8dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/route_address"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:textSize="12sp"
+ tools:text="This is a description of an amazing system route." />
+
+ </RelativeLayout>
+
+ </androidx.cardview.widget.CardView>
+
+</FrameLayout>
+
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..f46edb8 100644
--- a/samples/MediaRoutingDemo/src/main/res/values/strings.xml
+++ b/samples/MediaRoutingDemo/src/main/res/values/strings.xml
@@ -49,10 +49,18 @@
<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>
+
+ <string name="settings_activity_show_system_routes">Show system routes</string>
+
+ <string name="system_routing_activity_bluetooth_denied">Bluetooth sources will not be shown without access to Bluetooth</string>
</resources>
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 063496e..e4c8f9d 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])
@@ -1077,6 +1081,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/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/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/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/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/transition/transition/api/current.txt b/transition/transition/api/current.txt
index fdaded9..672d6db 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 fdaded9..672d6db 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 a344c51..187290f 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 1518fbe..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"), {
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/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/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/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/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java
index ff1bc0f..a8030ad 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/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-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-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-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/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/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-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 1f175a9..f53fb50 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/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/window/extensions/extensions/api/current.txt b/window/extensions/extensions/api/current.txt
index 490edb0..22888f8 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 android.util.DisplayMetrics getRearDisplayMetrics();
+ 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_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 public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
method @Deprecated public void setSplitInfoCallback(java.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 490edb0..22888f8 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 android.util.DisplayMetrics getRearDisplayMetrics();
+ 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_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 public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
method @Deprecated public void setSplitInfoCallback(java.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 490edb0..22888f8 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 android.util.DisplayMetrics getRearDisplayMetrics();
+ 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_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 public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
method @Deprecated public void setSplitInfoCallback(java.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 4941b4f2..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)
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..163d3af 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,16 @@
package androidx.window.extensions.area;
+import android.annotation.SuppressLint;
import android.app.Activity;
+import android.os.Build;
+import android.util.ArrayMap;
+import android.util.DisplayMetrics;
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 +51,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 +61,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 +89,8 @@
@IntDef({
STATUS_UNSUPPORTED,
STATUS_UNAVAILABLE,
- STATUS_AVAILABLE
+ STATUS_AVAILABLE,
+ STATUS_ACTIVE
})
@interface WindowAreaStatus {}
@@ -81,23 +102,39 @@
int SESSION_STATE_INACTIVE = 0;
/**
- * Session state constant to represent that there is an
- * active session currently in progress. Used by the library to
- * know when to return the session object to the developer when the
- * session is created and active.
+ * Session state constant to represent that there is currently an active session. The library
+ * uses this state to know when a session is created and active. Note that this state is
+ * different from SESSION_STATE_CONTENT_VISIBLE, because the presentation content in this state
+ * is not visible.
*/
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;
+
/** @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
})
@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 +145,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 +216,137 @@
* @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.");
+ }
+
+ /**
+ * Returns the {@link android.util.DisplayMetrics} associated with the rear facing display. If
+ * there is no rear facing display available on the device, returns an empty
+ * {@link android.util.DisplayMetrics} object.
+ *
+ * Since {@link WindowExtensions#VENDOR_API_LEVEL_3}
+ */
+ // TODO(b/273807238): Investigate how we can provide a listener to get runtime changes in
+ // rear display metrics to better support other form-factors in the future.
+ @NonNull
+ default DisplayMetrics getRearDisplayMetrics() {
+ 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/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 a5fe754..505a365 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/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 3ea0c05..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.WindowAreaController
-import androidx.window.area.WindowAreaSessionCallback
-import androidx.window.area.WindowAreaStatus
-import androidx.window.core.ExperimentalWindowApi
-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.flow.collect
-import kotlinx.coroutines.launch
-
-/**
- * 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-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 66dc2e8..b4d1e35 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 ff8eb13..3c78fd8 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -5,6 +5,94 @@
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";
+ field public static final String PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE";
+ field public static final String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES";
+ }
+
+}
+
+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 +213,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 412af2b..e28ac35 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -5,6 +5,94 @@
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";
+ field public static final String PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE";
+ field public static final String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES";
+ }
+
+}
+
+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 +107,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 +119,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.app.Activity activity);
+ method @androidx.window.core.ExperimentalWindowApi public static android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);
+ }
+
public final class ActivityFilter {
ctor public ActivityFilter(android.content.ComponentName componentName, String? intentAction);
method public android.content.ComponentName getComponentName();
@@ -132,7 +229,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 +246,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 ff8eb13..3c78fd8 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -5,6 +5,94 @@
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";
+ field public static final String PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE";
+ field public static final String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES";
+ }
+
+}
+
+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 +213,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 402be78..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,9 +77,9 @@
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 {
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/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..e0d60f16 100644
--- a/window/window/src/androidTest/AndroidManifest.xml
+++ b/window/window/src/androidTest/AndroidManifest.xml
@@ -28,5 +28,19 @@
<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" />
+ <property
+ android:name=
+ "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE"
+ android:value="false" />
+ <property
+ android:name=
+ "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES"
+ 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..57f5d51 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowPropertiesTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/WindowPropertiesTest.kt
@@ -21,6 +21,7 @@
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.test.ext.junit.rules.ActivityScenarioRule
+import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Rule
@@ -67,6 +68,62 @@
}
}
+ @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
+ )
+ )
+ }
+ }
+
+ @Test
+ fun test_property_allow_min_aspect_ratio_override() {
+ assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ // No-op, but to suppress lint
+ return
+ }
+ activityRule.scenario.onActivity { activity ->
+ // Should be false as defined in AndroidManifest.xml
+ assertFalse(
+ getProperty(
+ activity,
+ WindowProperties.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE
+ )
+ )
+ }
+ }
+
+ @Test
+ fun test_property_allow_resizeable_activity_overrides() {
+ assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ // No-op, but to suppress lint
+ return
+ }
+ activityRule.scenario.onActivity { activity ->
+ // Should be false as defined in AndroidManifest.xml
+ assertFalse(
+ getProperty(
+ activity,
+ WindowProperties
+ .PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES
+ )
+ )
+ }
+ }
+
@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 963966d..f6f80437 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,49 @@
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.test.ext.junit.rules.ActivityScenarioRule
import androidx.window.TestActivity
-import androidx.window.TestConsumer
import androidx.window.WindowTestUtils.Companion.assumeAtLeastVendorApiLevel
-import androidx.window.core.ExperimentalWindowApi
+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.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 kotlin.test.assertFailsWith
+import androidx.window.layout.WindowMetricsCalculator
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
import kotlinx.coroutines.ExperimentalCoroutinesApi
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 +67,121 @@
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 {
- assumeAtLeastVendorApiLevel(2)
+ public fun testRearFacingWindowAreaInfoList(): Unit = testScope.runTest {
+ assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.Q)
+ assumeAtLeastVendorApiLevel(3)
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 = 3
)
+ extensionComponent.currentRearDisplayStatus = STATUS_UNAVAILABLE
+ extensionComponent.currentRearDisplayPresentationStatus = STATUS_UNAVAILABLE
+ val collector = TestWindowAreaInfoListConsumer()
+ testScope.launch(Job()) {
+ controller.windowAreaInfos.collect(collector::accept)
+ }
+
+ val expectedAreaInfo = WindowAreaInfo(
+ metrics = WindowMetricsCalculator.fromDisplayMetrics(
+ extensionComponent.rearDisplayMetrics
+ ),
+ 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
+ )
+ val rearDisplayPresentationCapability = WindowAreaCapability(
+ OPERATION_PRESENT_ON_AREA,
+ WINDOW_AREA_STATUS_UNAVAILABLE
+ )
+ expectedAreaInfo.capabilityMap[OPERATION_TRANSFER_ACTIVITY_TO_AREA] =
+ rearDisplayCapability
+ expectedAreaInfo.capabilityMap[OPERATION_PRESENT_ON_AREA] =
+ rearDisplayPresentationCapability
+
+ assertEquals(listOf(expectedAreaInfo), collector.values[collector.values.size - 1])
+
+ extensionComponent
+ .updateRearDisplayStatusListeners(STATUS_AVAILABLE)
+
+ val updatedRearDisplayCapability = WindowAreaCapability(
+ OPERATION_TRANSFER_ACTIVITY_TO_AREA,
+ WINDOW_AREA_STATUS_AVAILABLE
+ )
+ expectedAreaInfo.capabilityMap[OPERATION_TRANSFER_ACTIVITY_TO_AREA] =
+ updatedRearDisplayCapability
+
+ assertEquals(listOf(expectedAreaInfo), collector.values[collector.values.size - 1])
+
+ // Update the presentation capability status and verify that only one window area
+ // info is still returned
+ extensionComponent.updateRearDisplayPresentationStatusListeners(STATUS_AVAILABLE)
+
+ val updatedRearDisplayPresentationCapability = WindowAreaCapability(
+ OPERATION_PRESENT_ON_AREA,
+ WINDOW_AREA_STATUS_AVAILABLE
+ )
+ expectedAreaInfo.capabilityMap[OPERATION_PRESENT_ON_AREA] =
+ updatedRearDisplayPresentationCapability
+
+ assertEquals(listOf(expectedAreaInfo), collector.values[collector.values.size - 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 {
- assumeAtLeastVendorApiLevel(2)
+ public fun testTransferToRearFacingWindowArea(): Unit = testScope.runTest {
+ assumeAtLeastVendorApiLevel(3)
val extensions = FakeWindowAreaComponent()
- val repo = WindowAreaControllerImpl(extensions)
- extensions.currentStatus = WindowAreaComponent.STATUS_AVAILABLE
+ val controller = WindowAreaControllerImpl(
+ windowAreaComponent = extensions,
+ vendorApiLevel = 3
+ )
+ 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 +191,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 +211,255 @@
}
}
- @TargetApi(Build.VERSION_CODES.N)
+ @RequiresApi(Build.VERSION_CODES.Q)
@Test
- public fun testRearDisplayModeReturnsError(): Unit = testScope.runTest {
- assumeAtLeastVendorApiLevel(2)
- val extensionComponent = FakeWindowAreaComponent()
- extensionComponent.currentStatus = WindowAreaComponent.STATUS_UNAVAILABLE
- val repo = WindowAreaControllerImpl(extensionComponent)
+ 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(3)
+ 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 testPresentRearDisplayArea(): 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_AVAILABLE)
+ 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 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_ACTIVE)
+ rearDisplayPresentationSessionConsumer?.accept(
+ WindowAreaComponent.SESSION_STATE_INACTIVE)
+ }
+
+ override fun getRearDisplayPresentation(): ExtensionWindowAreaPresentation? {
+ return TestExtensionWindowAreaPresentation(
+ testActivity!!,
+ rearDisplayPresentationSessionConsumer!!
+ )
+ }
+
+ override fun getRearDisplayMetrics(): DisplayMetrics {
+ return DisplayMetrics().apply {
+ widthPixels = 1080
+ heightPixels = 1080
+ densityDpi = 240
+ }
+ }
+
+ 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 +467,65 @@
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().apply {
+ widthPixels = 1080
+ heightPixels = 1080
+ densityDpi = 240
+ }
+ }
+ }
+
+ 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..532fd65
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt
@@ -0,0 +1,265 @@
+/*
+ * 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")
+ }
+
+ override fun getRearDisplayMetrics(): DisplayMetrics {
+ 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")
+ }
+
+ override fun getRearDisplayMetrics(): DisplayMetrics {
+ 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 6d76005..5aaf0aa 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
@@ -17,9 +17,13 @@
package androidx.window.embedding
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..6f73640 100644
--- a/window/window/src/main/java/androidx/window/WindowProperties.kt
+++ b/window/window/src/main/java/androidx/window/WindowProperties.kt
@@ -87,4 +87,99 @@
*/
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"
+
+ /**
+ * Application level
+ * [PackageManager][android.content.pm.PackageManager.Property] tag
+ * for an app to inform the system that the app should be opted-out from the compatibility
+ * override that changes the min aspect ratio.
+ *
+ * When this compat override is enabled the min aspect ratio given in the app's manifest can
+ * be overridden by the device manufacturer using their discretion to improve display
+ * compatibility unless the app's manifest value is higher. This treatment will also apply if
+ * no min aspect ratio value is provided in the manifest. These treatments can apply only in
+ * specific cases (e.g. device is in portrait) or each time the app is displayed on screen.
+ *
+ * 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_MIN_ASPECT_RATIO_OVERRIDE"
+ * android:value="false" />
+ * </application>
+ * ```
+ */
+ const val PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE =
+ "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE"
+
+ /**
+ * Application level
+ * [PackageManager][android.content.pm.PackageManager.Property] tag
+ * for an app to inform the system that the app should be opted-out from the
+ * compatibility overrides that change the resizability of the app.
+ *
+ * When these compat overrides are enabled they force the packages they are applied to to be
+ * resizable / unresizable. If the app is forced to be resizable this won't change whether
+ * the app can be put into multi-windowing mode, but allow the app to resize without going into
+ * size-compat mode when the window container resizes, such as display size change or screen
+ * rotation.
+ *
+ * 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_RESIZEABLE_ACTIVITY_OVERRIDES"
+ * android:value="false" />
+ * </application>
+ * ```
+ */
+ const val PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES =
+ "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES"
}
\ 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 de0971a..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
/**
- * 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..24e7152 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,31 @@
package androidx.window.area
-import androidx.window.core.ExperimentalWindowApi
+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.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
/**
* 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(
+ 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..e02ab52 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,113 @@
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.area.utils.DeviceUtils
import androidx.window.core.BuildConfig
-import androidx.window.core.ExperimentalWindowApi
+import androidx.window.core.ExtensionsUtil
import androidx.window.core.VerificationMode
-import androidx.window.extensions.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 +134,31 @@
@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 deviceSupported = Build.VERSION.SDK_INT > Build.VERSION_CODES.Q &&
+ windowAreaComponentExtensions != null &&
+ (ExtensionsUtil.safeVendorApiLevel >= 3 || DeviceUtils.hasDeviceMetrics(
+ Build.MANUFACTURER,
+ Build.MODEL
+ ))
+
val controller =
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N ||
- windowAreaComponentExtensions == null) {
- EmptyWindowAreaControllerImpl()
+ if (deviceSupported) {
+ WindowAreaControllerImpl(
+ windowAreaComponentExtensions!!,
+ ExtensionsUtil.safeVendorApiLevel
+ )
} else {
- WindowAreaControllerImpl(windowAreaComponentExtensions)
+ EmptyWindowAreaControllerImpl()
}
return decorator.decorate(controller)
}
@@ -116,7 +181,6 @@
* Decorator that allows us to provide different functionality
* in our window-testing artifact.
*/
-@ExperimentalWindowApi
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
interface WindowAreaControllerDecorator {
/**
@@ -126,7 +190,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..b31d974 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,29 @@
package androidx.window.area
import android.app.Activity
+import android.os.Binder
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
+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.area.utils.DeviceUtils
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_CONTENT_VISIBLE
import androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_INACTIVE
+import androidx.window.extensions.area.WindowAreaComponent.WindowAreaSessionState
import androidx.window.extensions.core.util.function.Consumer
+import androidx.window.layout.WindowMetrics
+import androidx.window.layout.WindowMetricsCalculator
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 +50,243 @@
* [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)
+ }
+
+ private fun updateRearDisplayAvailability(
+ status: @WindowAreaComponent.WindowAreaStatus Int
+ ) {
+ val windowMetrics = if (vendorApiLevel >= 3) {
+ WindowMetricsCalculator.fromDisplayMetrics(
+ displayMetrics = windowAreaComponent.rearDisplayMetrics
+ )
+ } else {
+ val displayMetrics = DeviceUtils.getRearDisplayMetrics(Build.MANUFACTURER, Build.MODEL)
+ if (displayMetrics != null) {
+ WindowMetricsCalculator.fromDisplayMetrics(
+ displayMetrics = displayMetrics
+ )
+ } else {
+ throw IllegalArgumentException(
+ "DeviceUtils rear display metrics entry should not be null"
+ )
}
- }.distinctUntilChanged()
+ }
+
+ currentRearDisplayModeStatus = WindowAreaAdapter.translate(status)
+ updateRearDisplayWindowArea(
+ WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA,
+ currentRearDisplayModeStatus,
+ windowMetrics
+ )
}
- override fun rearDisplayMode(
+ private fun updateRearDisplayPresentationAvailability(
+ extensionWindowAreaStatus: ExtensionWindowAreaStatus
+ ) {
+ currentRearDisplayPresentationStatus =
+ WindowAreaAdapter.translate(extensionWindowAreaStatus.windowAreaStatus)
+ val windowMetrics = WindowMetricsCalculator.fromDisplayMetrics(
+ displayMetrics = extensionWindowAreaStatus.windowAreaDisplayMetrics
+ )
+
+ updateRearDisplayWindowArea(
+ WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA,
+ currentRearDisplayPresentationStatus,
+ windowMetrics,
+ )
+ }
+
+ /**
+ * Updates the [WindowAreaInfo] object with the [REAR_DISPLAY_BINDER_DESCRIPTOR] binder token
+ * with the updated [status] corresponding to the [operation] and with the updated [metrics]
+ * received from the device for this window area.
+ *
+ * @param operation Operation that we are updating the status of.
+ * @param status New status for the operation provided on this window area.
+ * @param metrics Updated [WindowMetrics] for this window area.
+ */
+ 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
+ rearDisplayAreaInfo.metrics = metrics
+ currentWindowAreaInfoMap[REAR_DISPLAY_BINDER_DESCRIPTOR] = rearDisplayAreaInfo
+ }
+ }
+
+ /**
+ * 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 +306,58 @@
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> {
+
+ private var lastReportedSessionStatus: @WindowAreaSessionState Int = SESSION_STATE_INACTIVE
+ override fun accept(t: @WindowAreaSessionState Int) {
+ val previousStatus: @WindowAreaSessionState Int = lastReportedSessionStatus
+ lastReportedSessionStatus = t
+
+ executor.execute {
+ when (t) {
+ SESSION_STATE_ACTIVE -> {
+ // If the last status was visible, then ACTIVE infers the content is no
+ // longer visible.
+ if (previousStatus == SESSION_STATE_CONTENT_VISIBLE) {
+ windowAreaPresentationSessionCallback.onContainerVisibilityChanged(
+ false /* isVisible */
+ )
+ } else {
+ // Presentation should never be null if the session is active
+ windowAreaPresentationSessionCallback.onSessionStarted(
+ RearDisplayPresentationSessionPresenterImpl(
+ windowAreaComponent,
+ windowAreaComponent.rearDisplayPresentation!!
+ )
+ )
+ }
+ }
+
+ SESSION_STATE_CONTENT_VISIBLE ->
+ windowAreaPresentationSessionCallback.onContainerVisibilityChanged(true)
+
+ 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"
}
}
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..56c54aa
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi3Requirements.java
@@ -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 android.app.Activity;
+import android.util.DisplayMetrics;
+
+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();
+
+ /** @see WindowAreaComponent#getRearDisplayMetrics() */
+ @NonNull
+ DisplayMetrics getRearDisplayMetrics();
+}
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/area/utils/DeviceMetrics.kt b/window/window/src/main/java/androidx/window/area/utils/DeviceMetrics.kt
new file mode 100644
index 0000000..009d892
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/utils/DeviceMetrics.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.area.utils
+
+import android.os.Build
+import android.util.DisplayMetrics
+import androidx.annotation.RequiresApi
+
+/**
+ * Data class holding metrics about a specific device.
+ */
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+internal class DeviceMetrics(
+ val manufacturer: String,
+ val model: String,
+ val rearDisplayMetrics: DisplayMetrics
+) {
+ override fun equals(other: Any?): Boolean {
+ return other is DeviceMetrics &&
+ manufacturer == other.manufacturer &&
+ model == other.model &&
+ rearDisplayMetrics.equals(other.rearDisplayMetrics)
+ }
+
+ override fun hashCode(): Int {
+ var result = manufacturer.hashCode()
+ result = 31 * result + model.hashCode()
+ result = 31 * result + rearDisplayMetrics.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "DeviceMetrics{ Manufacturer: $manufacturer, model: $model, " +
+ "Rear display metrics: $rearDisplayMetrics }"
+ }
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/utils/DeviceUtils.kt b/window/window/src/main/java/androidx/window/area/utils/DeviceUtils.kt
new file mode 100644
index 0000000..44bb3fa
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/utils/DeviceUtils.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.window.area.utils
+
+import android.os.Build
+import android.util.DisplayMetrics
+import androidx.annotation.RequiresApi
+import java.util.Locale
+
+/**
+ * Utility object to provide information about specific devices that may not be available
+ * through the extensions API at a certain vendor API level
+ */
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+internal object DeviceUtils {
+
+ private val deviceList = listOf(DeviceMetrics("google", "pixel fold",
+ DisplayMetrics().apply {
+ widthPixels = 1080
+ heightPixels = 2092
+ density = 2.625f
+ densityDpi = 420 }
+ ))
+
+ internal fun hasDeviceMetrics(manufacturer: String, model: String): Boolean {
+ return deviceList.any {
+ it.manufacturer == manufacturer.lowercase(Locale.US) &&
+ it.model == model.lowercase(Locale.US)
+ }
+ }
+
+ internal fun getRearDisplayMetrics(manufacturer: String, model: String): DisplayMetrics? {
+ return deviceList.firstOrNull {
+ it.manufacturer == manufacturer.lowercase(Locale.US) &&
+ it.model == model.lowercase(Locale.US)
+ }?.rearDisplayMetrics
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/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 3340a8b..ce56396 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
@@ -20,10 +20,10 @@
import android.app.Activity
import android.content.Context
import android.content.Intent
+import android.os.Binder
import android.util.LayoutDirection
import android.util.Pair as AndroidPair
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.SplitPlaceholderRule.Builder as SplitPlaceholderRuleBuilder
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 d54b21a..a3559bb 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
@@ -17,16 +17,18 @@
package androidx.window.embedding
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/layout/WindowMetricsCalculator.kt b/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt
index 94dd455..132ff58 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt
@@ -20,12 +20,14 @@
import android.content.Context
import android.inputmethodservice.InputMethodService
import android.os.Build
+import android.util.DisplayMetrics
import android.view.Display
import android.view.WindowMetrics as AndroidWindowMetrics
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.annotation.UiContext
import androidx.core.view.WindowInsetsCompat
+import androidx.window.core.Bounds
/**
* An interface to calculate the [WindowMetrics] for an [Activity] or a [UiContext].
@@ -153,6 +155,13 @@
windowMetrics.bounds,
WindowInsetsCompat.toWindowInsetsCompat(windowMetrics.windowInsets)
)
+
+ internal fun fromDisplayMetrics(displayMetrics: DisplayMetrics): WindowMetrics {
+ return WindowMetrics(
+ Bounds(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels),
+ WindowInsetsCompat.Builder().build()
+ )
+ }
}
}
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-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"
+}