Merge "Implement UI-specific ClientProxy code generation." into androidx-main
diff --git a/activity/activity-ktx/api/current.ignore b/activity/activity-ktx/api/current.ignore
new file mode 100644
index 0000000..d23680b
--- /dev/null
+++ b/activity/activity-ktx/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: androidx.activity.OnBackPressedDispatcherKt:
+ Removed class androidx.activity.OnBackPressedDispatcherKt
diff --git a/activity/activity-ktx/api/current.txt b/activity/activity-ktx/api/current.txt
index eb507d0..ba9fdc3 100644
--- a/activity/activity-ktx/api/current.txt
+++ b/activity/activity-ktx/api/current.txt
@@ -6,10 +6,6 @@
method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM> viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
}
- public final class OnBackPressedDispatcherKt {
- method public static androidx.activity.OnBackPressedCallback addCallback(androidx.activity.OnBackPressedDispatcher, optional androidx.lifecycle.LifecycleOwner? owner, optional boolean enabled, kotlin.jvm.functions.Function1<? super androidx.activity.OnBackPressedCallback,kotlin.Unit> onBackPressed);
- }
-
public final class PipHintTrackerKt {
}
diff --git a/activity/activity-ktx/api/public_plus_experimental_current.txt b/activity/activity-ktx/api/public_plus_experimental_current.txt
index 29f1554..cece293 100644
--- a/activity/activity-ktx/api/public_plus_experimental_current.txt
+++ b/activity/activity-ktx/api/public_plus_experimental_current.txt
@@ -6,10 +6,6 @@
method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM> viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
}
- public final class OnBackPressedDispatcherKt {
- method public static androidx.activity.OnBackPressedCallback addCallback(androidx.activity.OnBackPressedDispatcher, optional androidx.lifecycle.LifecycleOwner? owner, optional boolean enabled, kotlin.jvm.functions.Function1<? super androidx.activity.OnBackPressedCallback,kotlin.Unit> onBackPressed);
- }
-
public final class PipHintTrackerKt {
method @RequiresApi(android.os.Build.VERSION_CODES.O) @kotlinx.coroutines.ExperimentalCoroutinesApi public static suspend Object? trackPipAnimationHintView(android.app.Activity, android.view.View view, kotlin.coroutines.Continuation<? super kotlin.Unit>);
}
diff --git a/activity/activity-ktx/api/restricted_current.ignore b/activity/activity-ktx/api/restricted_current.ignore
new file mode 100644
index 0000000..d23680b
--- /dev/null
+++ b/activity/activity-ktx/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: androidx.activity.OnBackPressedDispatcherKt:
+ Removed class androidx.activity.OnBackPressedDispatcherKt
diff --git a/activity/activity-ktx/api/restricted_current.txt b/activity/activity-ktx/api/restricted_current.txt
index eb507d0..ba9fdc3 100644
--- a/activity/activity-ktx/api/restricted_current.txt
+++ b/activity/activity-ktx/api/restricted_current.txt
@@ -6,10 +6,6 @@
method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<? extends VM> viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.viewmodel.CreationExtras>? extrasProducer, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
}
- public final class OnBackPressedDispatcherKt {
- method public static androidx.activity.OnBackPressedCallback addCallback(androidx.activity.OnBackPressedDispatcher, optional androidx.lifecycle.LifecycleOwner? owner, optional boolean enabled, kotlin.jvm.functions.Function1<? super androidx.activity.OnBackPressedCallback,kotlin.Unit> onBackPressed);
- }
-
public final class PipHintTrackerKt {
}
diff --git a/activity/activity-ktx/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt b/activity/activity-ktx/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
deleted file mode 100644
index 0a08e5f..0000000
--- a/activity/activity-ktx/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.activity
-
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.testing.TestLifecycleOwner
-import androidx.test.annotation.UiThreadTest
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertWithMessage
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class OnBackPressedDispatcherTest {
-
- private lateinit var dispatcher: OnBackPressedDispatcher
-
- @Before
- fun setup() {
- dispatcher = OnBackPressedDispatcher()
- }
-
- @UiThreadTest
- @Test
- fun testRegisterCallback() {
- var count = 0
- val callback = dispatcher.addCallback {
- count++
- }
- assertWithMessage("Callback should be enabled by default")
- .that(callback.isEnabled)
- .isTrue()
- assertWithMessage("Dispatcher should have an enabled callback")
- .that(dispatcher.hasEnabledCallbacks())
- .isTrue()
-
- dispatcher.onBackPressed()
-
- assertWithMessage("Count should be incremented after onBackPressed")
- .that(count)
- .isEqualTo(1)
- }
-
- @UiThreadTest
- @Test
- fun testRegisterDisabledCallback() {
- var count = 0
- val callback = dispatcher.addCallback(enabled = false) {
- count++
- }
- assertWithMessage("Callback should be disabled by default")
- .that(callback.isEnabled)
- .isFalse()
- assertWithMessage("Dispatcher should not have an enabled callback")
- .that(dispatcher.hasEnabledCallbacks())
- .isFalse()
-
- callback.isEnabled = true
-
- assertWithMessage("Dispatcher should have an enabled callback after setting isEnabled")
- .that(dispatcher.hasEnabledCallbacks())
- .isTrue()
- }
-
- @UiThreadTest
- @Test
- fun testLifecycleCallback() {
- val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.INITIALIZED)
- var count = 0
- dispatcher.addCallback(lifecycleOwner) {
- count++
- }
- assertWithMessage("Handler should return false if the Lifecycle isn't started")
- .that(dispatcher.hasEnabledCallbacks())
- .isFalse()
- dispatcher.onBackPressed()
- assertWithMessage("Non-started callbacks shouldn't have their count incremented")
- .that(count)
- .isEqualTo(0)
-
- // Now start the Lifecycle
- lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START)
- dispatcher.onBackPressed()
- assertWithMessage("Once the callbacks is started, the count should increment")
- .that(count)
- .isEqualTo(1)
-
- // Now stop the Lifecycle
- lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
- assertWithMessage("Handler should return false if the Lifecycle isn't started")
- .that(dispatcher.hasEnabledCallbacks())
- .isFalse()
- dispatcher.onBackPressed()
- assertWithMessage("Non-started callbacks shouldn't have their count incremented")
- .that(count)
- .isEqualTo(1)
- }
-
- @UiThreadTest
- @Test
- fun testIsEnabledWithinCallback() {
- var count = 0
- val callback = dispatcher.addCallback {
- count++
- isEnabled = false
- }
- assertWithMessage("Callback should be enabled by default")
- .that(callback.isEnabled)
- .isTrue()
- assertWithMessage("Dispatcher should have an enabled callback")
- .that(dispatcher.hasEnabledCallbacks())
- .isTrue()
-
- dispatcher.onBackPressed()
-
- assertWithMessage("Count should be incremented after onBackPressed")
- .that(count)
- .isEqualTo(1)
- assertWithMessage("Callback should be disabled after onBackPressed()")
- .that(callback.isEnabled)
- .isFalse()
- assertWithMessage("Dispatcher should have no enabled callbacks")
- .that(dispatcher.hasEnabledCallbacks())
- .isFalse()
- }
-
- @UiThreadTest
- @Test
- fun testRemoveWithinCallback() {
- var count = 0
- dispatcher.addCallback {
- count++
- remove()
- }
-
- dispatcher.onBackPressed()
-
- assertWithMessage("Count should be incremented after onBackPressed")
- .that(count)
- .isEqualTo(1)
- assertWithMessage("Dispatcher should have no enabled callbacks after remove")
- .that(dispatcher.hasEnabledCallbacks())
- .isFalse()
- }
-}
diff --git a/activity/activity-ktx/src/main/java/androidx/activity/OnBackPressedDispatcher.kt b/activity/activity-ktx/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
deleted file mode 100644
index 298e5ff..0000000
--- a/activity/activity-ktx/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.activity
-
-import androidx.lifecycle.LifecycleOwner
-
-/**
- * Create and add a new [OnBackPressedCallback] that calls [onBackPressed] in
- * [OnBackPressedCallback.handleOnBackPressed].
- *
- * If an [owner] is specified, the callback will only be added when the Lifecycle is
- * [androidx.lifecycle.Lifecycle.State.STARTED].
- *
- * A default [enabled] state can be supplied.
- */
-public fun OnBackPressedDispatcher.addCallback(
- owner: LifecycleOwner? = null,
- enabled: Boolean = true,
- onBackPressed: OnBackPressedCallback.() -> Unit
-): OnBackPressedCallback {
- val callback = object : OnBackPressedCallback(enabled) {
- override fun handleOnBackPressed() {
- onBackPressed()
- }
- }
- if (owner != null) {
- addCallback(owner, callback)
- } else {
- addCallback(callback)
- }
- return callback
-}
diff --git a/activity/activity/api/current.txt b/activity/activity/api/current.txt
index 078f6f9..b7e30ab 100644
--- a/activity/activity/api/current.txt
+++ b/activity/activity/api/current.txt
@@ -84,13 +84,17 @@
}
public final class OnBackPressedDispatcher {
+ ctor public OnBackPressedDispatcher(optional Runnable? fallbackOnBackPressed);
ctor public OnBackPressedDispatcher();
- ctor public OnBackPressedDispatcher(Runnable?);
- method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback);
- method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
+ method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback onBackPressedCallback);
+ method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner owner, androidx.activity.OnBackPressedCallback onBackPressedCallback);
method @MainThread public boolean hasEnabledCallbacks();
method @MainThread public void onBackPressed();
- method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher invoker);
+ }
+
+ public final class OnBackPressedDispatcherKt {
+ method public static androidx.activity.OnBackPressedCallback addCallback(androidx.activity.OnBackPressedDispatcher, optional androidx.lifecycle.LifecycleOwner? owner, optional boolean enabled, kotlin.jvm.functions.Function1<? super androidx.activity.OnBackPressedCallback,kotlin.Unit> onBackPressed);
}
public interface OnBackPressedDispatcherOwner extends androidx.lifecycle.LifecycleOwner {
diff --git a/activity/activity/api/public_plus_experimental_current.txt b/activity/activity/api/public_plus_experimental_current.txt
index 078f6f9..b7e30ab 100644
--- a/activity/activity/api/public_plus_experimental_current.txt
+++ b/activity/activity/api/public_plus_experimental_current.txt
@@ -84,13 +84,17 @@
}
public final class OnBackPressedDispatcher {
+ ctor public OnBackPressedDispatcher(optional Runnable? fallbackOnBackPressed);
ctor public OnBackPressedDispatcher();
- ctor public OnBackPressedDispatcher(Runnable?);
- method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback);
- method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
+ method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback onBackPressedCallback);
+ method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner owner, androidx.activity.OnBackPressedCallback onBackPressedCallback);
method @MainThread public boolean hasEnabledCallbacks();
method @MainThread public void onBackPressed();
- method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher invoker);
+ }
+
+ public final class OnBackPressedDispatcherKt {
+ method public static androidx.activity.OnBackPressedCallback addCallback(androidx.activity.OnBackPressedDispatcher, optional androidx.lifecycle.LifecycleOwner? owner, optional boolean enabled, kotlin.jvm.functions.Function1<? super androidx.activity.OnBackPressedCallback,kotlin.Unit> onBackPressed);
}
public interface OnBackPressedDispatcherOwner extends androidx.lifecycle.LifecycleOwner {
diff --git a/activity/activity/api/restricted_current.txt b/activity/activity/api/restricted_current.txt
index 575b8b9..474f211 100644
--- a/activity/activity/api/restricted_current.txt
+++ b/activity/activity/api/restricted_current.txt
@@ -83,13 +83,17 @@
}
public final class OnBackPressedDispatcher {
+ ctor public OnBackPressedDispatcher(optional Runnable? fallbackOnBackPressed);
ctor public OnBackPressedDispatcher();
- ctor public OnBackPressedDispatcher(Runnable?);
- method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback);
- method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
+ method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback onBackPressedCallback);
+ method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner owner, androidx.activity.OnBackPressedCallback onBackPressedCallback);
method @MainThread public boolean hasEnabledCallbacks();
method @MainThread public void onBackPressed();
- method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher invoker);
+ }
+
+ public final class OnBackPressedDispatcherKt {
+ method public static androidx.activity.OnBackPressedCallback addCallback(androidx.activity.OnBackPressedDispatcher, optional androidx.lifecycle.LifecycleOwner? owner, optional boolean enabled, kotlin.jvm.functions.Function1<? super androidx.activity.OnBackPressedCallback,kotlin.Unit> onBackPressed);
}
public interface OnBackPressedDispatcherOwner extends androidx.lifecycle.LifecycleOwner {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
index 7380772..51cf3de 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
@@ -82,6 +82,34 @@
@UiThreadTest
@Test
+ fun testIsEnabledWithinCallback() {
+ var count = 0
+ val callback = dispatcher.addCallback {
+ count++
+ isEnabled = false
+ }
+ assertWithMessage("Callback should be enabled by default")
+ .that(callback.isEnabled)
+ .isTrue()
+ assertWithMessage("Dispatcher should have an enabled callback")
+ .that(dispatcher.hasEnabledCallbacks())
+ .isTrue()
+
+ dispatcher.onBackPressed()
+
+ assertWithMessage("Count should be incremented after onBackPressed")
+ .that(count)
+ .isEqualTo(1)
+ assertWithMessage("Callback should be disabled after onBackPressed()")
+ .that(callback.isEnabled)
+ .isFalse()
+ assertWithMessage("Dispatcher should have no enabled callbacks")
+ .that(dispatcher.hasEnabledCallbacks())
+ .isFalse()
+ }
+
+ @UiThreadTest
+ @Test
fun testRemove() {
val onBackPressedCallback = CountingOnBackPressedCallback()
@@ -148,6 +176,25 @@
@UiThreadTest
@Test
+ fun testRemoveWithinCallback() {
+ var count = 0
+ dispatcher.addCallback {
+ count++
+ remove()
+ }
+
+ dispatcher.onBackPressed()
+
+ assertWithMessage("Count should be incremented after onBackPressed")
+ .that(count)
+ .isEqualTo(1)
+ assertWithMessage("Dispatcher should have no enabled callbacks after remove")
+ .that(dispatcher.hasEnabledCallbacks())
+ .isFalse()
+ }
+
+ @UiThreadTest
+ @Test
fun testMultipleCalls() {
val onBackPressedCallback = CountingOnBackPressedCallback()
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt b/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt
index 7174a2a..8568d78 100644
--- a/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt
+++ b/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt
@@ -16,9 +16,6 @@
package androidx.activity
import androidx.annotation.MainThread
-import androidx.annotation.OptIn
-import androidx.core.os.BuildCompat
-import androidx.core.util.Consumer
import java.util.concurrent.CopyOnWriteArrayList
/**
@@ -53,17 +50,14 @@
*/
@get:MainThread
@set:MainThread
- @set:OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
var isEnabled: Boolean = enabled
set(value) {
field = value
- if (enabledConsumer != null) {
- enabledConsumer!!.accept(field)
- }
+ enabledChangedCallback?.invoke()
}
private val cancellables = CopyOnWriteArrayList<Cancellable>()
- private var enabledConsumer: Consumer<Boolean>? = null
+ internal var enabledChangedCallback: (() -> Unit)? = null
/**
* Removes this callback from any [OnBackPressedDispatcher] it is currently
@@ -87,9 +81,4 @@
internal fun removeCancellable(cancellable: Cancellable) {
cancellables.remove(cancellable)
}
-
- @JvmName("setIsEnabledConsumer")
- internal fun setIsEnabledConsumer(isEnabled: Consumer<Boolean>?) {
- enabledConsumer = isEnabled
- }
}
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java
deleted file mode 100644
index 10c865e..0000000
--- a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java
+++ /dev/null
@@ -1,347 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.activity;
-
-import android.annotation.SuppressLint;
-import android.os.Build;
-import android.window.OnBackInvokedCallback;
-import android.window.OnBackInvokedDispatcher;
-
-import androidx.annotation.DoNotInline;
-import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.OptIn;
-import androidx.annotation.RequiresApi;
-import androidx.core.os.BuildCompat;
-import androidx.core.util.Consumer;
-import androidx.lifecycle.Lifecycle;
-import androidx.lifecycle.LifecycleEventObserver;
-import androidx.lifecycle.LifecycleOwner;
-
-import java.util.ArrayDeque;
-import java.util.Iterator;
-
-/**
- * Dispatcher that can be used to register {@link OnBackPressedCallback} instances for handling
- * the {@link ComponentActivity#onBackPressed()} callback via composition.
- * <pre>
- * public class FormEntryFragment extends Fragment {
- * {@literal @}Override
- * public void onAttach({@literal @}NonNull Context context) {
- * super.onAttach(context);
- * OnBackPressedCallback callback = new OnBackPressedCallback(
- * true // default to enabled
- * ) {
- * {@literal @}Override
- * public void handleOnBackPressed() {
- * showAreYouSureDialog();
- * }
- * };
- * requireActivity().getOnBackPressedDispatcher().addCallback(
- * this, // LifecycleOwner
- * callback);
- * }
- * }
- * </pre>
- */
-public final class OnBackPressedDispatcher {
-
- @Nullable
- private final Runnable mFallbackOnBackPressed;
-
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final ArrayDeque<OnBackPressedCallback> mOnBackPressedCallbacks = new ArrayDeque<>();
-
- private Consumer<Boolean> mEnabledConsumer;
-
- private OnBackInvokedCallback mOnBackInvokedCallback;
- private OnBackInvokedDispatcher mInvokedDispatcher;
- private boolean mBackInvokedCallbackRegistered = false;
-
- /**
- * Sets the {@link OnBackInvokedDispatcher} for handling system back for Android SDK T+.
- *
- * @param invoker the OnBackInvokedDispatcher to be set on this dispatcher
- */
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- public void setOnBackInvokedDispatcher(@NonNull OnBackInvokedDispatcher invoker) {
- mInvokedDispatcher = invoker;
- updateBackInvokedCallbackState();
- }
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- void updateBackInvokedCallbackState() {
- boolean shouldBeRegistered = hasEnabledCallbacks();
- if (mInvokedDispatcher != null) {
- if (shouldBeRegistered && !mBackInvokedCallbackRegistered) {
- Api33Impl.registerOnBackInvokedCallback(
- mInvokedDispatcher,
- OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- mOnBackInvokedCallback
- );
- mBackInvokedCallbackRegistered = true;
- } else if (!shouldBeRegistered && mBackInvokedCallbackRegistered) {
- Api33Impl.unregisterOnBackInvokedCallback(mInvokedDispatcher,
- mOnBackInvokedCallback);
- mBackInvokedCallbackRegistered = false;
- }
- }
- }
-
- /**
- * Create a new OnBackPressedDispatcher that dispatches System back button pressed events
- * to one or more {@link OnBackPressedCallback} instances.
- */
- public OnBackPressedDispatcher() {
- this(null);
- }
-
- /**
- * Create a new OnBackPressedDispatcher that dispatches System back button pressed events
- * to one or more {@link OnBackPressedCallback} instances.
- *
- * @param fallbackOnBackPressed The Runnable that should be triggered if
- * {@link #onBackPressed()} is called when {@link #hasEnabledCallbacks()} returns false.
- */
- @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
- public OnBackPressedDispatcher(@Nullable Runnable fallbackOnBackPressed) {
- mFallbackOnBackPressed = fallbackOnBackPressed;
- if (BuildCompat.isAtLeastT()) {
- mEnabledConsumer = aBoolean -> {
- if (BuildCompat.isAtLeastT()) {
- updateBackInvokedCallbackState();
- }
- };
- mOnBackInvokedCallback = Api33Impl.createOnBackInvokedCallback(this::onBackPressed);
- }
- }
-
- /**
- * Add a new {@link OnBackPressedCallback}. Callbacks are invoked in the reverse order in which
- * they are added, so this newly added {@link OnBackPressedCallback} will be the first
- * callback to receive a callback if {@link #onBackPressed()} is called.
- * <p>
- * This method is <strong>not</strong> {@link Lifecycle} aware - if you'd like to ensure that
- * you only get callbacks when at least {@link Lifecycle.State#STARTED started}, use
- * {@link #addCallback(LifecycleOwner, OnBackPressedCallback)}. It is expected that you
- * call {@link OnBackPressedCallback#remove()} to manually remove your callback.
- *
- * @param onBackPressedCallback The callback to add
- *
- * @see #onBackPressed()
- */
- @MainThread
- public void addCallback(@NonNull OnBackPressedCallback onBackPressedCallback) {
- addCancellableCallback(onBackPressedCallback);
- }
-
- /**
- * Internal implementation of {@link #addCallback(OnBackPressedCallback)} that gives
- * access to the {@link Cancellable} that specifically removes this callback from
- * the dispatcher without relying on {@link OnBackPressedCallback#remove()} which
- * is what external developers should be using.
- *
- * @param onBackPressedCallback The callback to add
- * @return a {@link Cancellable} which can be used to {@link Cancellable#cancel() cancel}
- * the callback and remove it from the set of OnBackPressedCallbacks.
- */
- @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- @MainThread
- @NonNull
- Cancellable addCancellableCallback(@NonNull OnBackPressedCallback onBackPressedCallback) {
- mOnBackPressedCallbacks.add(onBackPressedCallback);
- OnBackPressedCancellable cancellable = new OnBackPressedCancellable(onBackPressedCallback);
- onBackPressedCallback.addCancellable(cancellable);
- if (BuildCompat.isAtLeastT()) {
- updateBackInvokedCallbackState();
- onBackPressedCallback.setIsEnabledConsumer(mEnabledConsumer);
- }
- return cancellable;
- }
-
- /**
- * Receive callbacks to a new {@link OnBackPressedCallback} when the given
- * {@link LifecycleOwner} is at least {@link Lifecycle.State#STARTED started}.
- * <p>
- * This will automatically call {@link #addCallback(OnBackPressedCallback)} and
- * remove the callback as the lifecycle state changes.
- * As a corollary, if your lifecycle is already at least
- * {@link Lifecycle.State#STARTED started}, calling this method will result in an immediate
- * call to {@link #addCallback(OnBackPressedCallback)}.
- * <p>
- * When the {@link LifecycleOwner} is {@link Lifecycle.State#DESTROYED destroyed}, it will
- * automatically be removed from the list of callbacks. The only time you would need to
- * manually call {@link OnBackPressedCallback#remove()} is if
- * you'd like to remove the callback prior to destruction of the associated lifecycle.
- *
- * <p>
- * If the Lifecycle is already {@link Lifecycle.State#DESTROYED destroyed}
- * when this method is called, the callback will not be added.
- *
- * @param owner The LifecycleOwner which controls when the callback should be invoked
- * @param onBackPressedCallback The callback to add
- *
- * @see #onBackPressed()
- */
- @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
- @SuppressLint("LambdaLast")
- @MainThread
- public void addCallback(@NonNull LifecycleOwner owner,
- @NonNull OnBackPressedCallback onBackPressedCallback) {
- Lifecycle lifecycle = owner.getLifecycle();
- if (lifecycle.getCurrentState() == Lifecycle.State.DESTROYED) {
- return;
- }
-
- onBackPressedCallback.addCancellable(
- new LifecycleOnBackPressedCancellable(lifecycle, onBackPressedCallback));
- if (BuildCompat.isAtLeastT()) {
- updateBackInvokedCallbackState();
- onBackPressedCallback.setIsEnabledConsumer(mEnabledConsumer);
- }
- }
-
- /**
- * Checks if there is at least one {@link OnBackPressedCallback#isEnabled enabled}
- * callback registered with this dispatcher.
- *
- * @return True if there is at least one enabled callback.
- */
- @MainThread
- public boolean hasEnabledCallbacks() {
- Iterator<OnBackPressedCallback> iterator =
- mOnBackPressedCallbacks.descendingIterator();
- while (iterator.hasNext()) {
- if (iterator.next().isEnabled()) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Trigger a call to the currently added {@link OnBackPressedCallback callbacks} in reverse
- * order in which they were added. Only if the most recently added callback is not
- * {@link OnBackPressedCallback#isEnabled() enabled}
- * will any previously added callback be called.
- * <p>
- * If {@link #hasEnabledCallbacks()} is <code>false</code> when this method is called, the
- * fallback Runnable set by {@link #OnBackPressedDispatcher(Runnable) the constructor}
- * will be triggered.
- */
- @MainThread
- public void onBackPressed() {
- Iterator<OnBackPressedCallback> iterator =
- mOnBackPressedCallbacks.descendingIterator();
- while (iterator.hasNext()) {
- OnBackPressedCallback callback = iterator.next();
- if (callback.isEnabled()) {
- callback.handleOnBackPressed();
- return;
- }
- }
- if (mFallbackOnBackPressed != null) {
- mFallbackOnBackPressed.run();
- }
- }
-
- private class OnBackPressedCancellable implements Cancellable {
- private final OnBackPressedCallback mOnBackPressedCallback;
- OnBackPressedCancellable(OnBackPressedCallback onBackPressedCallback) {
- mOnBackPressedCallback = onBackPressedCallback;
- }
-
- @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
- @Override
- public void cancel() {
- mOnBackPressedCallbacks.remove(mOnBackPressedCallback);
- mOnBackPressedCallback.removeCancellable(this);
- if (BuildCompat.isAtLeastT()) {
- mOnBackPressedCallback.setIsEnabledConsumer(null);
- updateBackInvokedCallbackState();
- }
- }
- }
-
- private class LifecycleOnBackPressedCancellable implements LifecycleEventObserver,
- Cancellable {
- private final Lifecycle mLifecycle;
- private final OnBackPressedCallback mOnBackPressedCallback;
-
- @Nullable
- private Cancellable mCurrentCancellable;
-
- LifecycleOnBackPressedCancellable(@NonNull Lifecycle lifecycle,
- @NonNull OnBackPressedCallback onBackPressedCallback) {
- mLifecycle = lifecycle;
- mOnBackPressedCallback = onBackPressedCallback;
- lifecycle.addObserver(this);
- }
-
- @Override
- public void onStateChanged(@NonNull LifecycleOwner source,
- @NonNull Lifecycle.Event event) {
- if (event == Lifecycle.Event.ON_START) {
- mCurrentCancellable = addCancellableCallback(mOnBackPressedCallback);
- } else if (event == Lifecycle.Event.ON_STOP) {
- // Should always be non-null
- if (mCurrentCancellable != null) {
- mCurrentCancellable.cancel();
- }
- } else if (event == Lifecycle.Event.ON_DESTROY) {
- cancel();
- }
- }
-
- @Override
- public void cancel() {
- mLifecycle.removeObserver(this);
- mOnBackPressedCallback.removeCancellable(this);
- if (mCurrentCancellable != null) {
- mCurrentCancellable.cancel();
- mCurrentCancellable = null;
- }
- }
- }
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- static class Api33Impl {
- private Api33Impl() { }
-
- @DoNotInline
- static void registerOnBackInvokedCallback(
- Object dispatcher, int priority, Object callback
- ) {
- OnBackInvokedDispatcher onBackInvokedDispatcher = (OnBackInvokedDispatcher) dispatcher;
- OnBackInvokedCallback onBackInvokedCallback = (OnBackInvokedCallback) callback;
- onBackInvokedDispatcher.registerOnBackInvokedCallback(priority, onBackInvokedCallback);
- }
-
- @DoNotInline
- static void unregisterOnBackInvokedCallback(Object dispatcher, Object callback) {
- OnBackInvokedDispatcher onBackInvokedDispatcher = (OnBackInvokedDispatcher) dispatcher;
- OnBackInvokedCallback onBackInvokedCallback = (OnBackInvokedCallback) callback;
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(onBackInvokedCallback);
- }
- @DoNotInline
- static OnBackInvokedCallback createOnBackInvokedCallback(Runnable runnable) {
- return runnable::run;
- }
- }
-}
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
new file mode 100644
index 0000000..5319d9e
--- /dev/null
+++ b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
@@ -0,0 +1,317 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.activity
+
+import android.os.Build
+import android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
+import androidx.annotation.DoNotInline
+import androidx.annotation.MainThread
+import androidx.annotation.RequiresApi
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
+
+/**
+ * Dispatcher that can be used to register [OnBackPressedCallback] instances for handling
+ * the [ComponentActivity.onBackPressed] callback via composition.
+ *
+ * ```
+ * class FormEntryFragment : Fragment() {
+ * override fun onAttach(context: Context) {
+ * super.onAttach(context)
+ * val callback = object : OnBackPressedCallback(
+ * true // default to enabled
+ * ) {
+ * override fun handleOnBackPressed() {
+ * showAreYouSureDialog()
+ * }
+ * }
+ * requireActivity().onBackPressedDispatcher.addCallback(
+ * this, // LifecycleOwner
+ * callback
+ * )
+ * }
+ * }
+ * ```
+ *
+ * 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
+) {
+ private val onBackPressedCallbacks = ArrayDeque<OnBackPressedCallback>()
+ private var enabledChangedCallback: (() -> Unit)? = null
+ private var onBackInvokedCallback: OnBackInvokedCallback? = null
+ private var invokedDispatcher: OnBackInvokedDispatcher? = null
+ private var backInvokedCallbackRegistered = false
+
+ /**
+ * Sets the [OnBackInvokedDispatcher] for handling system back for Android SDK T+.
+ *
+ * @param invoker the OnBackInvokedDispatcher to be set on this dispatcher
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ fun setOnBackInvokedDispatcher(invoker: OnBackInvokedDispatcher) {
+ invokedDispatcher = invoker
+ updateBackInvokedCallbackState()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ internal fun updateBackInvokedCallbackState() {
+ val shouldBeRegistered = hasEnabledCallbacks()
+ val dispatcher = invokedDispatcher
+ val onBackInvokedCallback = onBackInvokedCallback
+ if (dispatcher != null && onBackInvokedCallback != null) {
+ if (shouldBeRegistered && !backInvokedCallbackRegistered) {
+ Api33Impl.registerOnBackInvokedCallback(
+ dispatcher,
+ OnBackInvokedDispatcher.PRIORITY_DEFAULT,
+ onBackInvokedCallback
+ )
+ backInvokedCallbackRegistered = true
+ } else if (!shouldBeRegistered && backInvokedCallbackRegistered) {
+ Api33Impl.unregisterOnBackInvokedCallback(
+ dispatcher,
+ onBackInvokedCallback
+ )
+ backInvokedCallbackRegistered = false
+ }
+ }
+ }
+
+ init {
+ if (Build.VERSION.SDK_INT >= 33) {
+ enabledChangedCallback = {
+ updateBackInvokedCallbackState()
+ }
+ onBackInvokedCallback = Api33Impl.createOnBackInvokedCallback { onBackPressed() }
+ }
+ }
+
+ /**
+ * Add a new [OnBackPressedCallback]. Callbacks are invoked in the reverse order in which
+ * they are added, so this newly added [OnBackPressedCallback] will be the first
+ * callback to receive a callback if [onBackPressed] is called.
+ *
+ * This method is **not** [Lifecycle] aware - if you'd like to ensure that
+ * you only get callbacks when at least [started][Lifecycle.State.STARTED], use
+ * [addCallback]. It is expected that you
+ * call [OnBackPressedCallback.remove] to manually remove your callback.
+ *
+ * @param onBackPressedCallback The callback to add
+ *
+ * @see onBackPressed
+ */
+ @MainThread
+ fun addCallback(onBackPressedCallback: OnBackPressedCallback) {
+ addCancellableCallback(onBackPressedCallback)
+ }
+
+ /**
+ * Internal implementation of [addCallback] that gives
+ * access to the [Cancellable] that specifically removes this callback from
+ * the dispatcher without relying on [OnBackPressedCallback.remove] which
+ * is what external developers should be using.
+ *
+ * @param onBackPressedCallback The callback to add
+ * @return a [Cancellable] which can be used to [cancel][Cancellable.cancel]
+ * the callback and remove it from the set of OnBackPressedCallbacks.
+ */
+ @MainThread
+ internal fun addCancellableCallback(onBackPressedCallback: OnBackPressedCallback): Cancellable {
+ onBackPressedCallbacks.add(onBackPressedCallback)
+ val cancellable = OnBackPressedCancellable(onBackPressedCallback)
+ onBackPressedCallback.addCancellable(cancellable)
+ if (Build.VERSION.SDK_INT >= 33) {
+ updateBackInvokedCallbackState()
+ onBackPressedCallback.enabledChangedCallback = enabledChangedCallback
+ }
+ return cancellable
+ }
+
+ /**
+ * Receive callbacks to a new [OnBackPressedCallback] when the given
+ * [LifecycleOwner] is at least [started][Lifecycle.State.STARTED].
+ *
+ * This will automatically call [addCallback] and remove the callback as the lifecycle
+ * state changes. As a corollary, if your lifecycle is already at least
+ * [started][Lifecycle.State.STARTED], calling this method will result in an immediate
+ * call to [addCallback].
+ *
+ * When the [LifecycleOwner] is [destroyed][Lifecycle.State.DESTROYED], it will
+ * automatically be removed from the list of callbacks. The only time you would need to
+ * manually call [OnBackPressedCallback.remove] is if
+ * you'd like to remove the callback prior to destruction of the associated lifecycle.
+ *
+ * If the Lifecycle is already [destroyed][Lifecycle.State.DESTROYED]
+ * when this method is called, the callback will not be added.
+ *
+ * @param owner The LifecycleOwner which controls when the callback should be invoked
+ * @param onBackPressedCallback The callback to add
+ *
+ * @see onBackPressed
+ */
+ @MainThread
+ fun addCallback(
+ owner: LifecycleOwner,
+ onBackPressedCallback: OnBackPressedCallback
+ ) {
+ val lifecycle = owner.lifecycle
+ if (lifecycle.currentState === Lifecycle.State.DESTROYED) {
+ return
+ }
+ onBackPressedCallback.addCancellable(
+ LifecycleOnBackPressedCancellable(lifecycle, onBackPressedCallback)
+ )
+ if (Build.VERSION.SDK_INT >= 33) {
+ updateBackInvokedCallbackState()
+ onBackPressedCallback.enabledChangedCallback = enabledChangedCallback
+ }
+ }
+
+ /**
+ * Checks 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
+ }
+
+ /**
+ * Trigger a call to the currently added [callbacks][OnBackPressedCallback] in reverse
+ * order in which they were added. Only if the most recently added callback is not
+ * [enabled][OnBackPressedCallback.isEnabled]
+ * will any previously added callback be called.
+ *
+ * If [hasEnabledCallbacks] is `false` when this method is called, the
+ * [fallbackOnBackPressed] set by the constructor will be triggered.
+ */
+ @MainThread
+ fun onBackPressed() {
+ val callback = onBackPressedCallbacks.lastOrNull {
+ it.isEnabled
+ }
+ if (callback != null) {
+ callback.handleOnBackPressed()
+ return
+ }
+ fallbackOnBackPressed?.run()
+ }
+
+ 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()
+ }
+ }
+ }
+
+ private inner class LifecycleOnBackPressedCancellable(
+ private val lifecycle: Lifecycle,
+ private val onBackPressedCallback: OnBackPressedCallback
+ ) : LifecycleEventObserver, Cancellable {
+ private var currentCancellable: Cancellable? = null
+
+ init {
+ lifecycle.addObserver(this)
+ }
+
+ override fun onStateChanged(
+ source: LifecycleOwner,
+ event: Lifecycle.Event
+ ) {
+ if (event === Lifecycle.Event.ON_START) {
+ currentCancellable = addCancellableCallback(onBackPressedCallback)
+ } else if (event === Lifecycle.Event.ON_STOP) {
+ // Should always be non-null
+ currentCancellable?.cancel()
+ } else if (event === Lifecycle.Event.ON_DESTROY) {
+ cancel()
+ }
+ }
+
+ override fun cancel() {
+ lifecycle.removeObserver(this)
+ onBackPressedCallback.removeCancellable(this)
+ currentCancellable?.cancel()
+ currentCancellable = null
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ internal object Api33Impl {
+ @DoNotInline
+ fun registerOnBackInvokedCallback(
+ dispatcher: Any,
+ priority: Int,
+ callback: Any
+ ) {
+ val onBackInvokedDispatcher = dispatcher as OnBackInvokedDispatcher
+ val onBackInvokedCallback = callback as OnBackInvokedCallback
+ onBackInvokedDispatcher.registerOnBackInvokedCallback(priority, onBackInvokedCallback)
+ }
+
+ @DoNotInline
+ fun unregisterOnBackInvokedCallback(dispatcher: Any, callback: Any) {
+ val onBackInvokedDispatcher = dispatcher as OnBackInvokedDispatcher
+ val onBackInvokedCallback = callback as OnBackInvokedCallback
+ onBackInvokedDispatcher.unregisterOnBackInvokedCallback(onBackInvokedCallback)
+ }
+
+ @DoNotInline
+ fun createOnBackInvokedCallback(onBackInvoked: () -> Unit): OnBackInvokedCallback {
+ return OnBackInvokedCallback { onBackInvoked() }
+ }
+ }
+}
+
+/**
+ * Create and add a new [OnBackPressedCallback] that calls [onBackPressed] in
+ * [OnBackPressedCallback.handleOnBackPressed].
+ *
+ * If an [owner] is specified, the callback will only be added when the Lifecycle is
+ * [androidx.lifecycle.Lifecycle.State.STARTED].
+ *
+ * A default [enabled] state can be supplied.
+ */
+@Suppress("RegistrationName")
+fun OnBackPressedDispatcher.addCallback(
+ owner: LifecycleOwner? = null,
+ enabled: Boolean = true,
+ onBackPressed: OnBackPressedCallback.() -> Unit
+): OnBackPressedCallback {
+ val callback = object : OnBackPressedCallback(enabled) {
+ override fun handleOnBackPressed() {
+ onBackPressed()
+ }
+ }
+ if (owner != null) {
+ addCallback(owner, callback)
+ } else {
+ addCallback(callback)
+ }
+ return callback
+}
\ No newline at end of file
diff --git a/appactions/interaction/interaction-service/api/current.txt b/appactions/interaction/interaction-service/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-service/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-service/api/public_plus_experimental_current.txt b/appactions/interaction/interaction-service/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-service/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-service/api/res-current.txt b/appactions/interaction/interaction-service/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/appactions/interaction/interaction-service/api/res-current.txt
diff --git a/appactions/interaction/interaction-service/api/restricted_current.txt b/appactions/interaction/interaction-service/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-service/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-service/build.gradle b/appactions/interaction/interaction-service/build.gradle
new file mode 100644
index 0000000..fa19659
--- /dev/null
+++ b/appactions/interaction/interaction-service/build.gradle
@@ -0,0 +1,36 @@
+/*
+ * 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")
+}
+
+dependencies {
+}
+
+android {
+ namespace "androidx.appactions.interaction.service"
+}
+
+androidx {
+ name = "androidx.appactions.interaction:interaction-service"
+ type = LibraryType.PUBLISHED_LIBRARY
+ inceptionYear = "2023"
+ description = "Library for integrating with Google Assistant via GRPC binder channel."
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/build.gradle b/benchmark/baseline-profiles-gradle-plugin/build.gradle
new file mode 100644
index 0000000..a3cfd33
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/build.gradle
@@ -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.
+ */
+
+import androidx.build.*
+
+plugins {
+ id("AndroidXPlugin")
+ id("kotlin")
+ id("java-gradle-plugin")
+}
+
+dependencies {
+ implementation(gradleApi())
+ implementation(libs.androidGradlePluginz)
+ implementation(libs.kotlinGradlePluginz)
+ implementation(libs.kotlinStdlib)
+ implementation(libs.protobuf)
+ implementation(libs.agpTestingPlatformCoreProto)
+
+ testImplementation(gradleTestKit())
+ testImplementation(project(":internal-testutils-gradle-plugin"))
+ testImplementation(libs.junit)
+ testImplementation(libs.kotlinTest)
+ testImplementation(libs.truth)
+}
+
+SdkResourceGenerator.generateForHostTest(project)
+
+gradlePlugin {
+ plugins {
+ baselineProfilesProducer {
+ id = "androidx.baselineprofiles.producer"
+ implementationClass = "androidx.baselineprofiles.gradle.producer.BaselineProfilesProducerPlugin"
+ }
+ baselineProfilesConsumer {
+ id = "androidx.baselineprofiles.consumer"
+ implementationClass = "androidx.baselineprofiles.gradle.consumer.BaselineProfilesConsumerPlugin"
+ }
+ baselineProfilesBuildProvider {
+ id = "androidx.baselineprofiles.buildprovider"
+ implementationClass = "androidx.baselineprofiles.gradle.buildprovider.BaselineProfilesBuildProviderPlugin"
+ }
+ }
+}
+
+androidx {
+ name = "Android Baseline Profiles Gradle Plugin"
+ publish = Publish.SNAPSHOT_ONLY
+ type = LibraryType.GRADLE_PLUGIN
+ inceptionYear = "2022"
+ description = "Android Baseline Profiles Gradle Plugin"
+}
+
+tasks {
+ validatePlugins {
+ failOnWarning.set(true)
+ enableStricterValidation.set(true)
+ }
+}
\ No newline at end of file
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/buildprovider/BaselineProfilesApkProviderExtension.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/buildprovider/BaselineProfilesApkProviderExtension.kt
new file mode 100644
index 0000000..b05ac08
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/buildprovider/BaselineProfilesApkProviderExtension.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.baselineprofiles.gradle.buildprovider
+
+import org.gradle.api.Project
+
+/**
+ * Allows specifying settings for the Baseline Profiles Plugin.
+ */
+open class BaselineProfilesApkProviderExtension {
+
+ companion object {
+
+ private const val EXTENSION_NAME = "baselineProfilesApkProvider"
+
+ internal fun registerExtension(project: Project): BaselineProfilesApkProviderExtension {
+ val ext = project
+ .extensions.findByType(BaselineProfilesApkProviderExtension::class.java)
+ if (ext != null) {
+ return ext
+ }
+ return project
+ .extensions.create(EXTENSION_NAME, BaselineProfilesApkProviderExtension::class.java)
+ }
+ }
+
+ /**
+ * Keep rule file for the special build for baseline profiles. Note that this file is
+ * automatically generated by default, unless a path is specified here. The path is relative
+ * to the module directory. The same file is used for all the variants. There should be no
+ * need to customize this file.
+ */
+ var keepRulesFile: String? = null
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/buildprovider/BaselineProfilesBuildProviderPlugin.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/buildprovider/BaselineProfilesBuildProviderPlugin.kt
new file mode 100644
index 0000000..f8890ce
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/buildprovider/BaselineProfilesBuildProviderPlugin.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.baselineprofiles.gradle.buildprovider
+
+import androidx.baselineprofiles.gradle.utils.createNonObfuscatedBuildTypes
+import com.android.build.api.variant.ApplicationAndroidComponentsExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+
+/**
+ * This is the build provider plugin for baseline profile generation. In order to generate baseline
+ * profiles three plugins are needed: one is applied to the app or the library that should consume
+ * the baseline profile when building (consumer), one is applied to the project that should supply
+ * the test apk (build provider) and the last one is applied to a library module containing the ui
+ * test that generate the baseline profile on the device (producer).
+ *
+ * TODO (b/265438721): build provider should be changed to apk provider.
+ */
+class BaselineProfilesBuildProviderPlugin : Plugin<Project> {
+
+ override fun apply(project: Project) {
+ project.pluginManager.withPlugin("com.android.application") {
+ configureWithAndroidPlugin(project = project)
+ }
+ }
+
+ private fun configureWithAndroidPlugin(project: Project) {
+
+ // Prepares extensions used by the plugin
+ val baselineProfilesExtension =
+ BaselineProfilesApkProviderExtension.registerExtension(project)
+
+ val androidComponent = project.extensions.getByType(
+ ApplicationAndroidComponentsExtension::class.java
+ )
+
+ // Create the non obfuscated release build types from the existing release ones.
+ // We want to extend all the current release build types based on isDebuggable flag.
+ // The map created here maps the non obfuscated build types newly created to the release
+ // ones.
+ val extendedTypeToOriginalTypeMapping = mutableMapOf<String, String>()
+ androidComponent.finalizeDsl { applicationExtension ->
+
+ val debugBuildType = applicationExtension.buildTypes.getByName("debug")
+ createNonObfuscatedBuildTypes(
+ project = project,
+ extension = applicationExtension,
+ extendedBuildTypeToOriginalBuildTypeMapping = extendedTypeToOriginalTypeMapping,
+ filterBlock = { !it.isDebuggable },
+ configureBlock = {
+ isJniDebuggable = false
+ isDebuggable = false
+ isMinifyEnabled = true
+ isShrinkResources = true
+ isProfileable = true
+ signingConfig = debugBuildType.signingConfig
+ enableAndroidTestCoverage = false
+ enableUnitTestCoverage = false
+
+ // The keep rule file is added later in the variants callback so that we can
+ // generate it on-the-fly in the intermediates folder, if it wasn't specified in
+ // the config.
+ }
+ )
+ }
+
+ // Creates a task to generate the keep rule file
+ val genKeepRuleTaskProvider = project
+ .tasks
+ .register(
+ "generateBaselineProfilesKeepRules",
+ GenerateKeepRulesForBaselineProfilesTask::class.java
+ ) {
+ it.keepRuleFile.set(
+ project.layout.buildDirectory.file(
+ "intermediates/baselineprofiles/baseline-profile-keep-rules.pro"
+ )
+ )
+ }
+
+ val keepRuleFileProvider = genKeepRuleTaskProvider.flatMap { it.keepRuleFile }
+
+ // Sets the keep rule file for the baseline profile variants
+ androidComponent.apply {
+
+ onVariants {
+
+ // We can skip the build types that were NOT created by this plugin.
+ if (it.buildType !in extendedTypeToOriginalTypeMapping.keys) {
+ return@onVariants
+ }
+
+ // If the keep rule file was manually specified, then use that one
+ if (baselineProfilesExtension.keepRulesFile != null) {
+ it.proguardFiles.add(
+ project
+ .layout
+ .projectDirectory
+ .file(baselineProfilesExtension.keepRulesFile!!)
+ )
+ return@onVariants
+ }
+
+ // Otherwise the keep rule file is generated and added to the list of keep rule
+ // files for the variant.
+ it.proguardFiles.add(keepRuleFileProvider)
+ }
+ }
+ }
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/buildprovider/GenerateKeepRulesForBaselineProfilesTask.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/buildprovider/GenerateKeepRulesForBaselineProfilesTask.kt
new file mode 100644
index 0000000..f49e9d0
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/buildprovider/GenerateKeepRulesForBaselineProfilesTask.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.baselineprofiles.gradle.buildprovider
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+
+/**
+ * This task generates a fixed keep rule file that simply disables obfuscation.
+ * Applying this configuration to the baseline profiles build type ensures that we can produce
+ * a minified, non obfuscated release build.
+ */
+@CacheableTask
+abstract class GenerateKeepRulesForBaselineProfilesTask : DefaultTask() {
+
+ companion object {
+ private val KEEP_RULES = """
+ # Autogenerated for baseline profiles. Changes to this file will be overwritten.
+ -dontobfuscate
+
+ """.trimIndent()
+ }
+
+ @get:OutputFile
+ abstract val keepRuleFile: RegularFileProperty
+
+ init {
+ group = "Baseline Profiles"
+ description = "Generates the keep rules for the special baseline profiles rule."
+ }
+
+ @TaskAction
+ fun exec() {
+ keepRuleFile.get().asFile.writeText(KEEP_RULES)
+ logger.info("Generated keep rule file for baseline profiles build in ${keepRuleFile.get()}")
+ }
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerExtension.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerExtension.kt
new file mode 100644
index 0000000..79a547b
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerExtension.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.baselineprofiles.gradle.consumer
+
+import org.gradle.api.Project
+
+/**
+ * Allows specifying settings for the Baseline Profiles Plugin.
+ */
+open class BaselineProfilesConsumerExtension {
+
+ companion object {
+
+ private const val EXTENSION_NAME = "baselineProfilesProfileConsumer"
+
+ internal fun registerExtension(project: Project): BaselineProfilesConsumerExtension {
+ val ext = project.extensions.findByType(BaselineProfilesConsumerExtension::class.java)
+ if (ext != null) {
+ return ext
+ }
+ return project
+ .extensions.create(EXTENSION_NAME, BaselineProfilesConsumerExtension::class.java)
+ }
+ }
+
+ /**
+ * Specifies what build type should be used to generate baseline profiles. By default this build
+ * type is `release`. In general, this should be a build type used for distribution. Note that
+ * this will be deprecated when b/265438201 is fixed, as all the build types will be used to
+ * generate baseline profiles.
+ */
+ var buildTypeName: String = "release"
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerPlugin.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerPlugin.kt
new file mode 100644
index 0000000..06b97d9
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerPlugin.kt
@@ -0,0 +1,241 @@
+/*
+ * 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.baselineprofiles.gradle.consumer
+
+import androidx.baselineprofiles.gradle.utils.ATTRIBUTE_BUILD_TYPE
+import androidx.baselineprofiles.gradle.utils.ATTRIBUTE_CATEGORY_BASELINE_PROFILE
+import androidx.baselineprofiles.gradle.utils.ATTRIBUTE_FLAVOR
+import androidx.baselineprofiles.gradle.utils.CONFIGURATION_NAME_BASELINE_PROFILES
+import androidx.baselineprofiles.gradle.utils.camelCase
+import com.android.build.api.variant.AndroidComponentsExtension
+import com.android.build.gradle.AppExtension
+import com.android.build.gradle.LibraryExtension
+import com.android.build.gradle.TestedExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.attributes.Category
+import org.gradle.api.tasks.StopExecutionException
+
+/**
+ * This is the consumer plugin for baseline profile generation. In order to generate baseline
+ * profiles three plugins are needed: one is applied to the app or the library that should consume
+ * the baseline profile when building (consumer), one is applied to the project that should supply
+ * the apk under test (build provider) and the last one is applied to a library module containing
+ * the ui test that generate the baseline profile on the device (producer).
+ */
+class BaselineProfilesConsumerPlugin : Plugin<Project> {
+
+ companion object {
+
+ // The output file for the HRF baseline profile file in `src/main`
+ private const val BASELINE_PROFILE_SRC_MAIN_FILENAME = "baseline-prof.txt"
+ }
+
+ override fun apply(project: Project) {
+ project.pluginManager.withPlugin("com.android.application") {
+ configureWithAndroidPlugin(project = project, isApplication = true)
+ }
+ project.pluginManager.withPlugin("com.android.library") {
+ configureWithAndroidPlugin(project = project, isApplication = false)
+ }
+ }
+
+ private fun configureWithAndroidPlugin(project: Project, isApplication: Boolean) {
+
+ // TODO (b/259737859): This code will be updated to use source sets for baseline profiles,
+ // as soon androidx repo is updated to use AGP 8.0-beta01.
+
+ val androidComponent = project.extensions.getByType(
+ AndroidComponentsExtension::class.java
+ )
+
+ val baselineProfilesExtension = BaselineProfilesConsumerExtension.registerExtension(project)
+
+ // Creates all the configurations, one per variant.
+ // Note that for this version of the plugin is not possible to rely entirely on the variant
+ // api so the actual creation of the tasks is postponed to be executed when all the
+ // agp tasks have been created, using the old api.
+ val mainBaselineProfileConfiguration = createBaselineProfileConfigurationForVariant(
+ project,
+ variantName = "",
+ flavorName = "",
+ buildTypeName = "",
+ mainConfiguration = null
+ )
+ val baselineProfileConfigurations = mutableListOf<Configuration>()
+ val baselineProfileVariantNames = mutableListOf<String>()
+ androidComponent.apply {
+ onVariants {
+
+ // Only create configurations for the build type expressed in the baseline profiles
+ // extension. Note that this can be removed after b/265438201.
+ if (it.buildType != baselineProfilesExtension.buildTypeName) {
+ return@onVariants
+ }
+
+ baselineProfileConfigurations.add(
+ createBaselineProfileConfigurationForVariant(
+ project,
+ variantName = it.name,
+ flavorName = it.flavorName ?: "",
+ buildTypeName = it.buildType ?: "",
+ mainConfiguration = mainBaselineProfileConfiguration
+ )
+ )
+
+ // Save this variant name so later we can use it to set a dependency on the
+ // merge/prepare art profile task for it.
+ baselineProfileVariantNames.add(it.name)
+ }
+ }
+
+ // Now that the configurations are created, the tasks can be created. The consumer plugin
+ // can only be applied to either applications or libraries.
+ // Note that for this plugin does not use the new variant api as it tries to access to some
+ // AGP tasks that don't yet exist in the new variant api callback (b/262007432).
+ val extensionVariants =
+ when (val tested = project.extensions.getByType(TestedExtension::class.java)) {
+ is AppExtension -> tested.applicationVariants
+ is LibraryExtension -> tested.libraryVariants
+ else -> throw StopExecutionException(
+ """
+ Unrecognized extension: $tested not of type AppExtension or LibraryExtension.
+ """.trimIndent()
+ )
+ }
+
+ // After variants have been resolved and the AGP tasks have been created add the plugin tasks.
+ var applied = false
+ extensionVariants.all {
+ if (applied) return@all
+ applied = true
+
+ // Currently the plugin does not support generating a baseline profile for a specific
+ // flavor: all the flavors are merged into one and copied in src/main/baseline-prof.txt.
+ // This can be changed after b/239659205 when baseline profiles become a source set.
+ val mergeBaselineProfilesTaskProvider = project.tasks.register(
+ "generateBaselineProfiles", MergeBaselineProfileTask::class.java
+ ) { task ->
+
+ // These are all the configurations this task depends on, in order to consume their
+ // artifacts.
+ task.baselineProfileFileCollection.setFrom(baselineProfileConfigurations)
+
+ // This is the output file where all the configurations will be merged in.
+ // Note that this file is overwritten.
+ task.baselineProfileFile.set(
+ project
+ .layout
+ .projectDirectory
+ .file("src/main/$BASELINE_PROFILE_SRC_MAIN_FILENAME")
+ )
+ }
+
+ // If this is an application the mergeBaselineProfilesTask must run before the
+ // tasks that handle the baseline profile packaging. Merge for applications, prepare
+ // for libraries. Note that this will change with AGP 8.0 that should support
+ // source sets for baseline profiles.
+ for (variantName in baselineProfileVariantNames) {
+ val taskProvider = if (isApplication) {
+ project.tasks.named(camelCase("merge", variantName, "artProfile"))
+ } else {
+ project.tasks.named(camelCase("prepare", variantName, "artProfile"))
+ }
+ taskProvider.configure { it.mustRunAfter(mergeBaselineProfilesTaskProvider) }
+ }
+ }
+ }
+
+ private fun createBaselineProfileConfigurationForVariant(
+ project: Project,
+ variantName: String,
+ flavorName: String,
+ buildTypeName: String,
+ mainConfiguration: Configuration?
+ ): Configuration {
+
+ val buildTypeConfiguration =
+ if (buildTypeName.isNotBlank() && buildTypeName != variantName) {
+ project
+ .configurations
+ .maybeCreate(
+ camelCase(
+ buildTypeName,
+ CONFIGURATION_NAME_BASELINE_PROFILES
+ )
+ )
+ .apply {
+ if (mainConfiguration != null) extendsFrom(mainConfiguration)
+ isCanBeResolved = true
+ isCanBeConsumed = false
+ }
+ } else null
+
+ val flavorConfiguration = if (flavorName.isNotBlank() && flavorName != variantName) {
+ project
+ .configurations
+ .maybeCreate(camelCase(flavorName, CONFIGURATION_NAME_BASELINE_PROFILES))
+ .apply {
+ if (mainConfiguration != null) extendsFrom(mainConfiguration)
+ isCanBeResolved = true
+ isCanBeConsumed = false
+ }
+ } else null
+
+ return project
+ .configurations
+ .maybeCreate(camelCase(variantName, CONFIGURATION_NAME_BASELINE_PROFILES))
+ .apply {
+
+ // The variant specific configuration always extends from build type and flavor
+ // configurations, when existing.
+ val extendFrom = mutableListOf<Configuration>()
+ if (mainConfiguration != null) {
+ extendFrom.add(mainConfiguration)
+ }
+ if (flavorConfiguration != null) {
+ extendFrom.add(flavorConfiguration)
+ }
+ if (buildTypeConfiguration != null) {
+ extendFrom.add(buildTypeConfiguration)
+ }
+ setExtendsFrom(extendFrom)
+
+ isCanBeResolved = true
+ isCanBeConsumed = false
+
+ attributes {
+ it.attribute(
+ Category.CATEGORY_ATTRIBUTE,
+ project.objects.named(
+ Category::class.java,
+ ATTRIBUTE_CATEGORY_BASELINE_PROFILE
+ )
+ )
+ it.attribute(
+ ATTRIBUTE_BUILD_TYPE,
+ buildTypeName
+ )
+ it.attribute(
+ ATTRIBUTE_FLAVOR,
+ flavorName
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/MergeBaselineProfileTask.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/MergeBaselineProfileTask.kt
new file mode 100644
index 0000000..2277e63
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/consumer/MergeBaselineProfileTask.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.baselineprofiles.gradle.consumer
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+
+/**
+ * Collects all the baseline profile artifacts generated by all the producer configurations and
+ * merges them into one, sorting and ensuring that there are no duplicated lines.
+ *
+ * The format of the profile is a simple list of classes and methods loaded in memory when
+ * executing a test, expressed in JVM format. Duplicates can arise when multiple tests cover the
+ * same code: for example when having 2 tests both covering the startup path and then doing
+ * something else, both will have startup classes and methods. There is no harm in having this
+ * duplication but mostly the profile file will be unnecessarily larger.
+ */
+@CacheableTask
+abstract class MergeBaselineProfileTask : DefaultTask() {
+
+ @get:InputFiles
+ @get:PathSensitive(PathSensitivity.NONE)
+ abstract val baselineProfileFileCollection: ConfigurableFileCollection
+
+ @get:OutputFile
+ abstract val baselineProfileFile: RegularFileProperty
+
+ init {
+ group = "Baseline Profiles"
+ description = "Merges all the baseline profiles into one, removing duplicate lines."
+ }
+
+ @TaskAction
+ fun exec() {
+ val lines = baselineProfileFileCollection.files
+ .flatMap { it.readLines() }
+ .sorted()
+ .distinct()
+
+ baselineProfileFile.get().asFile.writeText(lines.joinToString(System.lineSeparator()))
+ }
+}
\ No newline at end of file
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerExtension.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerExtension.kt
new file mode 100644
index 0000000..e1222bf
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerExtension.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.baselineprofiles.gradle.producer
+
+import org.gradle.api.Project
+
+/**
+ * Allows specifying settings for the Baseline Profiles Plugin.
+ */
+open class BaselineProfilesProducerExtension {
+
+ companion object {
+
+ private const val EXTENSION_NAME = "baselineProfilesProfileProducer"
+
+ internal fun registerExtension(project: Project): BaselineProfilesProducerExtension {
+ val ext = project
+ .extensions.findByType(BaselineProfilesProducerExtension::class.java)
+ if (ext != null) {
+ return ext
+ }
+ return project
+ .extensions.create(EXTENSION_NAME, BaselineProfilesProducerExtension::class.java)
+ }
+ }
+
+ /**
+ * Allows selecting the managed devices to use for generating baseline profiles.
+ * This should be a list of strings contained the names of the devices specified in the
+ * configuration for managed devices. For example, in the following configuration, the name
+ * is `pixel6Api31`.
+ * ```
+ * testOptions.managedDevices.devices {
+ * pixel6Api31(ManagedVirtualDevice) {
+ * device = "Pixel 6"
+ * apiLevel = 31
+ * systemImageSource = "aosp"
+ * }
+ * }
+ * ```
+ */
+ var managedDevices = mutableListOf<String>()
+
+ /**
+ * Whether baseline profiles should be generated on connected devices.
+ */
+ var useConnectedDevices: Boolean = true
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerPlugin.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerPlugin.kt
new file mode 100644
index 0000000..0f60d71
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerPlugin.kt
@@ -0,0 +1,286 @@
+/*
+ * 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.baselineprofiles.gradle.producer
+
+import androidx.baselineprofiles.gradle.utils.ATTRIBUTE_BUILD_TYPE
+import androidx.baselineprofiles.gradle.utils.ATTRIBUTE_CATEGORY_BASELINE_PROFILE
+import androidx.baselineprofiles.gradle.utils.ATTRIBUTE_FLAVOR
+import androidx.baselineprofiles.gradle.utils.BUILD_TYPE_BASELINE_PROFILE_PREFIX
+import androidx.baselineprofiles.gradle.utils.CONFIGURATION_NAME_BASELINE_PROFILES
+import androidx.baselineprofiles.gradle.utils.camelCase
+import androidx.baselineprofiles.gradle.utils.createBuildTypeIfNotExists
+import androidx.baselineprofiles.gradle.utils.createNonObfuscatedBuildTypes
+import com.android.build.api.variant.TestAndroidComponentsExtension
+import com.android.build.gradle.TestExtension
+import org.gradle.api.GradleException
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.UnknownTaskException
+import org.gradle.api.attributes.Category
+
+/**
+ * This is the producer plugin for baseline profile generation. In order to generate baseline
+ * profiles three plugins are needed: one is applied to the app or the library that should consume
+ * the baseline profile when building (consumer), one is applied to the project that should supply
+ * the apk under test (build provider) and the last one is applied to a library module containing
+ * the ui test that generate the baseline profile on the device (producer).
+ */
+class BaselineProfilesProducerPlugin : Plugin<Project> {
+
+ override fun apply(project: Project) {
+ project.pluginManager.withPlugin("com.android.test") {
+ configureWithAndroidPlugin(project = project)
+ }
+ }
+
+ private fun configureWithAndroidPlugin(project: Project) {
+
+ // Prepares extensions used by the plugin
+ val baselineProfilesExtension =
+ BaselineProfilesProducerExtension.registerExtension(project)
+
+ val testAndroidComponent = project.extensions.getByType(
+ TestAndroidComponentsExtension::class.java
+ )
+
+ // We need the instrumentation apk to run as a separate process
+ val testExtension = project.extensions.getByType(TestExtension::class.java)
+ testExtension.experimentalProperties["android.experimental.self-instrumenting"] = true
+
+ // Creates the new build types to match the build provider. Note that release does not
+ // exist by default so we need to create nonObfuscatedRelease and map it manually to
+ // `release`. All the existing build types beside `debug`, that is the default one, are
+ // added manually in the configuration so we can assume they've been added for the purpose
+ // of generating baseline profiles. We don't need to create a nonObfuscated build type from
+ // `debug`.
+
+ val nonObfuscatedReleaseName = camelCase(BUILD_TYPE_BASELINE_PROFILE_PREFIX, "release")
+ val extendedTypeToOriginalTypeMapping = mutableMapOf(nonObfuscatedReleaseName to "release")
+
+ testAndroidComponent.finalizeDsl { ext ->
+
+ createNonObfuscatedBuildTypes(
+ project = project,
+ extension = ext,
+ extendedBuildTypeToOriginalBuildTypeMapping = extendedTypeToOriginalTypeMapping,
+ filterBlock = {
+ // TODO: Which build types to skip. In theory we want to skip only debug because
+ // it's the default one. All the ones that have been manually added should be
+ // considered for this.
+ it.name != "debug"
+ },
+ configureBlock = {
+ enableAndroidTestCoverage = false
+ enableUnitTestCoverage = false
+ },
+ )
+
+ createBuildTypeIfNotExists(
+ project = project,
+ extension = ext,
+ buildTypeName = nonObfuscatedReleaseName,
+ configureBlock = {
+ enableAndroidTestCoverage = false
+ enableUnitTestCoverage = false
+ matchingFallbacks += listOf("release")
+ }
+ )
+ }
+
+ // Makes sure that only the non obfuscated build type variant selected is enabled
+ testAndroidComponent.apply {
+ beforeVariants {
+ it.enable = it.buildType in extendedTypeToOriginalTypeMapping.keys
+ }
+ }
+
+ // Creates all the configurations, one per variant for the newly created build type.
+ // Note that for this version of the plugin is not possible to rely entirely on the variant
+ // api so the actual creation of the tasks is postponed to be executed when all the
+ // agp tasks have been created, using the old api.
+ val createTaskBlocks = mutableListOf<() -> (Unit)>()
+ testAndroidComponent.apply {
+
+ onVariants {
+
+ // Creating configurations only for the extended build types.
+ if (it.buildType == null ||
+ it.buildType !in extendedTypeToOriginalTypeMapping.keys) {
+ return@onVariants
+ }
+
+ // Creates the configuration to handle this variant. Note that in the attributes
+ // to match the configuration we use the original build type without `nonObfuscated`.
+ val originalBuildTypeName = extendedTypeToOriginalTypeMapping[it.buildType] ?: ""
+ val configurationName = createBaselineProfileConfigurationForVariant(
+ project = project,
+ variantName = it.name,
+ flavorName = it.flavorName ?: "",
+ originalBuildTypeName = originalBuildTypeName
+ )
+
+ // Prepares a block to execute later that creates the tasks for this variant
+ createTaskBlocks.add {
+ createTasksForVariant(
+ project = project,
+ variantName = it.name,
+ flavorName = it.flavorName ?: "",
+ configurationName = configurationName,
+ baselineProfilesExtension = baselineProfilesExtension
+ )
+ }
+ }
+ }
+
+ // After variants have been resolved and the AGP tasks have been created, create the plugin
+ // tasks.
+ var applied = false
+ testExtension.applicationVariants.all {
+ if (applied) return@all
+ applied = true
+ createTaskBlocks.forEach { it() }
+ }
+ }
+
+ private fun createTasksForVariant(
+ project: Project,
+ variantName: String,
+ flavorName: String,
+ configurationName: String,
+ baselineProfilesExtension: BaselineProfilesProducerExtension
+ ) {
+
+ // Prepares the devices list to use to generate baseline profiles.
+ val devices = mutableSetOf<String>()
+ .also { it.addAll(baselineProfilesExtension.managedDevices) }
+ if (baselineProfilesExtension.useConnectedDevices) {
+ devices.add("connected")
+ }
+
+ // Determines which test tasks should run based on configuration
+ val shouldExpectConnectedOutput = devices.contains("connected")
+ val shouldExpectManagedOutput = baselineProfilesExtension.managedDevices.isNotEmpty()
+
+ // The test task runs the ui tests
+ val testTasks = devices.map {
+ try {
+ project.tasks.named(camelCase(it, variantName, "androidTest"))
+ } catch (e: UnknownTaskException) {
+ throw GradleException(
+ """
+ It wasn't possible to determine the test task for managed device `$it`.
+ Please check the managed devices specified in the baseline profiles configuration.
+ """.trimIndent(), e
+ )
+ }
+ }
+
+ // Merge result protos task
+ val mergeResultProtosTask = project.tasks.named(
+ camelCase("merge", variantName, "testResultProtos")
+ )
+
+ // The collect task collects the baseline profile files from the ui test results
+ val collectTaskProvider = project.tasks.register(
+ camelCase("collect", variantName, "BaselineProfiles"),
+ CollectBaselineProfilesTask::class.java
+ ) {
+
+ // Test tasks have to run before collect
+ it.dependsOn(testTasks, mergeResultProtosTask)
+
+ // Sets flavor name
+ it.outputFile.set(
+ project
+ .layout
+ .buildDirectory
+ .file("intermediates/baselineprofiles/$flavorName/baseline-prof.txt")
+ )
+
+ // Sets the connected test results location, if tests are supposed to run also on
+ // connected devices.
+ if (shouldExpectConnectedOutput) {
+ it.connectedAndroidTestOutputDir.set(
+ if (flavorName.isEmpty()) {
+ project.layout.buildDirectory
+ .dir("outputs/androidTest-results/connected")
+ } else {
+ project.layout.buildDirectory
+ .dir("outputs/androidTest-results/connected/flavors/$flavorName")
+ }
+ )
+ }
+
+ // Sets the managed devices test results location, if tests are supposed to run
+ // also on managed devices.
+ if (shouldExpectManagedOutput) {
+ it.managedAndroidTestOutputDir.set(
+ if (flavorName.isEmpty()) {
+ project.layout.buildDirectory.dir(
+ "outputs/androidTest-results/managedDevice"
+ )
+ } else {
+ project.layout.buildDirectory.dir(
+ "outputs/androidTest-results/managedDevice/flavors/$flavorName"
+ )
+ }
+ )
+ }
+ }
+
+ // The artifacts are added to the configuration that exposes the generated baseline profile
+ project.artifacts { artifactHandler ->
+ artifactHandler.add(configurationName, collectTaskProvider) { artifact ->
+ artifact.builtBy(collectTaskProvider)
+ }
+ }
+ }
+
+ private fun createBaselineProfileConfigurationForVariant(
+ project: Project,
+ variantName: String,
+ flavorName: String,
+ originalBuildTypeName: String,
+ ): String {
+ val configurationName =
+ camelCase(variantName, CONFIGURATION_NAME_BASELINE_PROFILES)
+ project.configurations
+ .maybeCreate(configurationName)
+ .apply {
+ isCanBeResolved = false
+ isCanBeConsumed = true
+ attributes {
+ it.attribute(
+ Category.CATEGORY_ATTRIBUTE,
+ project.objects.named(
+ Category::class.java,
+ ATTRIBUTE_CATEGORY_BASELINE_PROFILE
+ )
+ )
+ it.attribute(
+ ATTRIBUTE_BUILD_TYPE,
+ originalBuildTypeName
+ )
+ it.attribute(
+ ATTRIBUTE_FLAVOR,
+ flavorName
+ )
+ }
+ }
+ return configurationName
+ }
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/CollectBaselineProfilesTask.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/CollectBaselineProfilesTask.kt
new file mode 100644
index 0000000..400554d
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/producer/CollectBaselineProfilesTask.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.baselineprofiles.gradle.producer
+
+import com.google.testing.platform.proto.api.core.TestSuiteResultProto
+import java.io.File
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.work.DisableCachingByDefault
+
+/**
+ * Collects the generated baseline profiles from the instrumentation results of a previous run of
+ * the ui tests.
+ */
+@DisableCachingByDefault(because = "Not worth caching.")
+abstract class CollectBaselineProfilesTask : DefaultTask() {
+
+ @get:Optional
+ @get:InputDirectory
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val connectedAndroidTestOutputDir: DirectoryProperty
+
+ @get:Optional
+ @get:InputDirectory
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val managedAndroidTestOutputDir: DirectoryProperty
+
+ @get:OutputFile
+ abstract val outputFile: RegularFileProperty
+
+ init {
+ group = "Baseline Profiles"
+ description = "Collects baseline profiles previously generated through integration tests."
+ }
+
+ @TaskAction
+ fun exec() {
+
+ // Prepares list with test results to read
+ val testResultProtoFiles =
+ listOf(connectedAndroidTestOutputDir, managedAndroidTestOutputDir)
+ .filter { it.isPresent }
+ .map { it.file("test-result.pb").get().asFile }
+
+ // A test-result.pb file must exist as output of connected and managed device tests.
+ // If it doesn't exist it's because there were no tests to run. If there are no devices,
+ // the test task will simply fail. The following check is to give a meaningful error
+ // message if something like that happens.
+ if (testResultProtoFiles.filter { !it.exists() }.isNotEmpty()) {
+ throw GradleException(
+ """
+ Expected test results were not found. This is most likely because there are no
+ tests to run. Please check that there are ui tests to execute. You can find more
+ information at https://d.android.com/studio/test/advanced-test-setup. To create a
+ baseline profile test instead, please check the documentation at
+ https://d.android.com/baselineprofiles.
+ """.trimIndent()
+ )
+ }
+
+ val profiles = mutableSetOf<String>()
+ testResultProtoFiles
+ .map { TestSuiteResultProto.TestSuiteResult.parseFrom(it.readBytes()) }
+ .forEach { testSuiteResult ->
+ for (testResult in testSuiteResult.testResultList) {
+
+ // Baseline profile files are extracted by the test task. Here we find their
+ // location checking the test-result.pb proto. Note that the BaselineProfileRule
+ // produces one baseline profile file per test.
+ val baselineProfileFiles = testResult.outputArtifactList
+ .filter {
+ // The label for this artifact is `additionaltestoutput.benchmark.trace`
+ // https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:utp/android-test-plugin-host-additional-test-output/src/main/java/com/android/tools/utp/plugins/host/additionaltestoutput/AndroidAdditionalTestOutputPlugin.kt;l=199?q=additionaltestoutput.benchmark.trace
+ it.label.label == "additionaltestoutput.benchmark.trace" &&
+ "-baseline-prof-" in it.sourcePath.path
+ }
+ .map { File(it.sourcePath.path) }
+ if (baselineProfileFiles.isEmpty()) {
+ continue
+ }
+
+ // Merge each baseline profile file from the test results into the aggregated
+ // baseline file, removing duplicate lines.
+ for (baselineProfileFile in baselineProfileFiles) {
+ profiles.addAll(baselineProfileFile.readLines())
+ }
+ }
+ }
+
+ if (profiles.isEmpty()) {
+ throw GradleException("No baseline profiles found in test outputs.")
+ }
+
+ // Saves the merged baseline profile file in the final destination
+ val file = outputFile.get().asFile
+ file.writeText(profiles.joinToString(System.lineSeparator()))
+ logger.info("Aggregated baseline profile generated at ${file.absolutePath}")
+ }
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/utils/BuildTypes.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/utils/BuildTypes.kt
new file mode 100644
index 0000000..81dfc24
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/utils/BuildTypes.kt
@@ -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.baselineprofiles.gradle.utils
+
+import com.android.build.api.dsl.BuildType
+import com.android.build.api.dsl.CommonExtension
+import org.gradle.api.GradleException
+import org.gradle.api.Project
+
+internal inline fun <reified T : BuildType> createNonObfuscatedBuildTypes(
+ project: Project,
+ extension: CommonExtension<*, T, *, *>,
+ crossinline filterBlock: (T) -> (Boolean),
+ crossinline configureBlock: T.() -> (Unit),
+ extendedBuildTypeToOriginalBuildTypeMapping: MutableMap<String, String>
+) {
+ extension.buildTypes
+ .filter { buildType ->
+ if (buildType !is T) {
+ throw GradleException(
+ "Build type `${buildType.name}` is not of type ${T::class}"
+ )
+ }
+ filterBlock(buildType)
+ }
+ .forEach { buildType ->
+
+ val newBuildTypeName = camelCase(BUILD_TYPE_BASELINE_PROFILE_PREFIX, buildType.name)
+
+ // Check in case the build type was created manually (to allow full customization)
+ if (extension.buildTypes.findByName(newBuildTypeName) != null) {
+ project.logger.info(
+ "Build type $newBuildTypeName won't be created because already exists."
+ )
+ } else {
+ // If the new build type doesn't exist, create it simply extending the configured
+ // one (by default release).
+ extension.buildTypes.create(newBuildTypeName).apply {
+ initWith(buildType)
+ matchingFallbacks += listOf(buildType.name)
+ configureBlock(this as T)
+ }
+ }
+ // Mapping the build type to the newly created
+ extendedBuildTypeToOriginalBuildTypeMapping[newBuildTypeName] = buildType.name
+ }
+}
+
+internal inline fun <reified T : BuildType> createBuildTypeIfNotExists(
+ project: Project,
+ extension: CommonExtension<*, T, *, *>,
+ buildTypeName: String,
+ configureBlock: BuildType.() -> Unit
+) {
+ // Check in case the build type was created manually (to allow full customization)
+ if (extension.buildTypes.findByName(buildTypeName) != null) {
+ project.logger.info(
+ "Build type $buildTypeName won't be created because already exists."
+ )
+ return
+ }
+ // If the new build type doesn't exist, create it simply extending the configured
+ // one (by default release).
+ extension.buildTypes.create(buildTypeName).apply {
+ configureBlock(this)
+ }
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/utils/Utils.kt b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/utils/Utils.kt
new file mode 100644
index 0000000..6af0e36
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin/androidx/baselineprofiles/gradle/utils/Utils.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.baselineprofiles.gradle.utils
+
+import org.gradle.api.attributes.Attribute
+import org.gradle.configurationcache.extensions.capitalized
+
+internal fun camelCase(vararg strings: String): String {
+ if (strings.isEmpty()) return ""
+ return StringBuilder()
+ .apply {
+ var shouldCapitalize = false
+ for (str in strings.filter { it.isNotBlank() }) {
+ append(if (shouldCapitalize) str.capitalized() else str)
+ shouldCapitalize = true
+ }
+ }.toString()
+}
+
+// Prefix for the build type baseline profiles
+internal const val BUILD_TYPE_BASELINE_PROFILE_PREFIX = "nonObfuscated"
+
+// Configuration consumed by this plugin that carries the baseline profile HRF file.
+internal const val CONFIGURATION_NAME_BASELINE_PROFILES = "baselineprofiles"
+
+// Custom category attribute to match the baseline profile configuration
+internal const val ATTRIBUTE_CATEGORY_BASELINE_PROFILE = "baselineprofile"
+
+internal val ATTRIBUTE_FLAVOR =
+ Attribute.of("androidx.baselineprofiles.gradle.attributes.Flavor", String::class.java)
+internal val ATTRIBUTE_BUILD_TYPE =
+ Attribute.of("androidx.baselineprofiles.gradle.attributes.BuildType", String::class.java)
\ No newline at end of file
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.buildprovider.gradle.properties b/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.buildprovider.gradle.properties
new file mode 100644
index 0000000..863bea9
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.buildprovider.gradle.properties
@@ -0,0 +1,16 @@
+#
+# 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.
+#
+implementation-class=androidx.baselineprofiles.gradle.buildprovider.BaselineProfilesBuildProviderPlugin
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.consumer.gradle.properties b/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.consumer.gradle.properties
new file mode 100644
index 0000000..46d0a3e
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.consumer.gradle.properties
@@ -0,0 +1,16 @@
+#
+# 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.
+#
+implementation-class=androidx.baselineprofiles.gradle.consumer.BaselineProfilesConsumerPlugin
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.producer.gradle.properties b/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.producer.gradle.properties
new file mode 100644
index 0000000..a7c0b60
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/main/resources/META-INF/gradle-plugins/androidx.baselineprofiles.producer.gradle.properties
@@ -0,0 +1,16 @@
+#
+# 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.
+#
+implementation-class=androidx.baselineprofiles.gradle.producer.BaselineProfilesProducerPlugin
\ No newline at end of file
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/buildprovider/BaselineProfilesBuildProviderPluginTest.kt b/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/buildprovider/BaselineProfilesBuildProviderPluginTest.kt
new file mode 100644
index 0000000..a215b11
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/buildprovider/BaselineProfilesBuildProviderPluginTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.baselineprofiles.gradle.buildprovider
+
+import androidx.testutils.gradle.ProjectSetupRule
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import org.gradle.testkit.runner.GradleRunner
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class BaselineProfilesBuildProviderPluginTest {
+
+ @get:Rule
+ val projectSetup = ProjectSetupRule()
+
+ private lateinit var gradleRunner: GradleRunner
+
+ @Before
+ fun setUp() {
+ gradleRunner = GradleRunner.create()
+ .withProjectDir(projectSetup.rootDir)
+ .withPluginClasspath()
+ }
+
+ @Test
+ fun verifyBuildType() {
+ projectSetup.writeDefaultBuildGradle(
+ prefix = """
+ plugins {
+ id("com.android.application")
+ id("androidx.baselineprofiles.buildprovider")
+ }
+ android {
+ namespace 'com.example.namespace'
+ }
+ tasks.register("printNonObfuscatedReleaseBuildType") {
+ android.buildTypes.nonObfuscatedRelease.properties.each {k,v->println(k+"="+v)}
+ }
+ """.trimIndent(),
+ suffix = ""
+ )
+
+ val buildTypeProperties = gradleRunner
+ .withArguments("printNonObfuscatedReleaseBuildType", "--stacktrace")
+ .build()
+ .output
+ .lines()
+
+ assertThat(buildTypeProperties).contains("shrinkResources=true")
+ assertThat(buildTypeProperties).contains("minifyEnabled=true")
+ assertThat(buildTypeProperties).contains("testCoverageEnabled=false")
+ assertThat(buildTypeProperties).contains("debuggable=false")
+ }
+
+ @Test
+ fun generateBaselineProfilesKeepRuleFile() {
+ projectSetup.writeDefaultBuildGradle(
+ prefix = """
+ plugins {
+ id("com.android.application")
+ id("androidx.baselineprofiles.buildprovider")
+ }
+ android {
+ namespace 'com.example.namespace'
+ }
+ """.trimIndent(),
+ suffix = ""
+ )
+
+ val outputLines = gradleRunner
+ .withArguments("generateBaselineProfilesKeepRules", "--stacktrace", "--info")
+ .build()
+ .output
+ .lines()
+
+ val find = "Generated keep rule file for baseline profiles build in"
+ val proguardFilePath = outputLines
+ .first { it.startsWith(find) }
+ .split(find)[1]
+ .trim()
+ val proguardContent = File(proguardFilePath)
+ .readText()
+ .lines()
+ .filter { !it.startsWith("#") && it.isNotBlank() }
+ assertThat(proguardContent).containsExactly("-dontobfuscate")
+ }
+}
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerPluginTest.kt b/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerPluginTest.kt
new file mode 100644
index 0000000..6490dbb
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/consumer/BaselineProfilesConsumerPluginTest.kt
@@ -0,0 +1,220 @@
+/*
+ * 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.baselineprofiles.gradle.consumer
+
+import androidx.baselineprofiles.gradle.utils.CONFIGURATION_NAME_BASELINE_PROFILES
+import androidx.baselineprofiles.gradle.utils.camelCase
+import androidx.testutils.gradle.ProjectSetupRule
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import org.gradle.testkit.runner.GradleRunner
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class BaselineProfilesConsumerPluginTest {
+
+ // To test the consumer plugin we need a module that exposes a baselineprofiles configuration
+ // to be consumed. This is why we'll be using 2 projects. The producer project build gradle
+ // is generated ad hoc in the tests that require it in order to supply mock profiles.
+
+ private val rootFolder = TemporaryFolder().also { it.create() }
+
+ @get:Rule
+ val consumerProjectSetup = ProjectSetupRule(rootFolder.root)
+
+ @get:Rule
+ val producerProjectSetup = ProjectSetupRule(rootFolder.root)
+
+ private lateinit var consumerModuleName: String
+ private lateinit var producerModuleName: String
+ private lateinit var gradleRunner: GradleRunner
+
+ @Before
+ fun setUp() {
+ consumerModuleName = consumerProjectSetup.rootDir.relativeTo(rootFolder.root).name
+ producerModuleName = producerProjectSetup.rootDir.relativeTo(rootFolder.root).name
+
+ rootFolder.newFile("settings.gradle").writeText(
+ """
+ include '$consumerModuleName'
+ include '$producerModuleName'
+ """.trimIndent()
+ )
+ gradleRunner = GradleRunner.create()
+ .withProjectDir(consumerProjectSetup.rootDir)
+ .withPluginClasspath()
+ }
+
+ @Test
+ fun testGenerateBaselineProfilesTaskWithNoFlavors() {
+ consumerProjectSetup.writeDefaultBuildGradle(
+ prefix = """
+ plugins {
+ id("com.android.library")
+ id("androidx.baselineprofiles.consumer")
+ }
+ android {
+ namespace 'com.example.namespace'
+ }
+ dependencies {
+ baselineprofiles(project(":$producerModuleName"))
+ }
+ """.trimIndent(),
+ suffix = ""
+ )
+ producerProjectSetup.writeDefaultBuildGradle(
+ prefix = MockProducerBuildGrade()
+ .withConfiguration(flavor = "", buildType = "release")
+ .withProducedBaselineProfiles(listOf("3", "2"), flavor = "", buildType = "release")
+ .withProducedBaselineProfiles(listOf("4", "1"), flavor = "", buildType = "release")
+ .build(),
+ suffix = ""
+ )
+
+ gradleRunner
+ .withArguments("generateBaselineProfiles", "--stacktrace")
+ .build()
+
+ // The expected output should have each line sorted descending
+ assertThat(
+ File(consumerProjectSetup.rootDir, "src/main/baseline-prof.txt").readLines()
+ )
+ .containsExactly("4", "3", "2", "1")
+ }
+
+ @Test
+ fun testGenerateBaselineProfilesTaskWithFlavors() {
+ consumerProjectSetup.writeDefaultBuildGradle(
+ prefix = """
+ plugins {
+ id("com.android.application")
+ id("androidx.baselineprofiles.consumer")
+ }
+ android {
+ namespace 'com.example.namespace'
+ productFlavors {
+ flavorDimensions = ["version"]
+ free {
+ dimension "version"
+ }
+ paid {
+ dimension "version"
+ }
+ }
+ }
+ dependencies {
+ baselineprofiles(project(":$producerModuleName"))
+ }
+ """.trimIndent(),
+ suffix = ""
+ )
+ producerProjectSetup.writeDefaultBuildGradle(
+ prefix = MockProducerBuildGrade()
+ .withConfiguration(flavor = "free", buildType = "release")
+ .withConfiguration(flavor = "paid", buildType = "release")
+ .withProducedBaselineProfiles(
+ listOf("3", "2"),
+ flavor = "free",
+ buildType = "release"
+ )
+ .withProducedBaselineProfiles(
+ listOf("4", "1"),
+ flavor = "paid",
+ buildType = "release"
+ )
+ .build(),
+ suffix = ""
+ )
+
+ gradleRunner
+ .withArguments("generateBaselineProfiles", "--stacktrace")
+ .build()
+
+ // The expected output should have each line sorted ascending
+ val baselineProf =
+ File(consumerProjectSetup.rootDir, "src/main/baseline-prof.txt").readLines()
+ assertThat(baselineProf).containsExactly("1", "2", "3", "4")
+ }
+}
+
+private class MockProducerBuildGrade() {
+
+ private var profileIndex = 0
+ private var content = """
+ plugins { id("com.android.library") }
+ android { namespace 'com.example.namespace' }
+
+ // This task produces a file with a fixed output
+ abstract class TestProfileTask extends DefaultTask {
+ @Input abstract Property<String> getFileContent()
+ @OutputFile abstract RegularFileProperty getOutputFile()
+ @TaskAction void exec() { getOutputFile().get().asFile.write(getFileContent().get()) }
+ }
+
+ """.trimIndent()
+
+ fun withConfiguration(flavor: String, buildType: String): MockProducerBuildGrade {
+
+ content += """
+
+ configurations {
+ ${configurationName(flavor, buildType)} {
+ canBeConsumed = true
+ canBeResolved = false
+ attributes {
+ attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, "baselineprofile"))
+ attribute(Attribute.of("androidx.baselineprofiles.gradle.attributes.BuildType", String), "$buildType")
+ attribute(Attribute.of("androidx.baselineprofiles.gradle.attributes.Flavor", String), "$flavor")
+ }
+ }
+ }
+
+ """.trimIndent()
+ return this
+ }
+
+ fun withProducedBaselineProfiles(
+ lines: List<String>,
+ flavor: String = "",
+ buildType: String
+ ): MockProducerBuildGrade {
+ profileIndex++
+ content += """
+
+ def task$profileIndex = tasks.register('testProfile$profileIndex', TestProfileTask)
+ task$profileIndex.configure {
+ it.outputFile.set(project.layout.buildDirectory.file("test$profileIndex"))
+ it.fileContent.set(${"\"\"\"${lines.joinToString("\n")}\"\"\""})
+ }
+ artifacts {
+ add("${configurationName(flavor, buildType)}", task$profileIndex.map { it.outputFile })
+ }
+
+ """.trimIndent()
+ return this
+ }
+
+ fun build() = content
+}
+
+private fun configurationName(flavor: String, buildType: String): String =
+ camelCase(flavor, buildType, CONFIGURATION_NAME_BASELINE_PROFILES)
diff --git a/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerPluginTest.kt b/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerPluginTest.kt
new file mode 100644
index 0000000..2de52a4
--- /dev/null
+++ b/benchmark/baseline-profiles-gradle-plugin/src/test/kotlin/androidx/baselineprofiles/gradle/producer/BaselineProfilesProducerPluginTest.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.baselineprofiles.gradle.producer
+
+import androidx.testutils.gradle.ProjectSetupRule
+import kotlin.test.assertTrue
+import org.gradle.testkit.runner.GradleRunner
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class BaselineProfilesProducerPluginTest {
+
+ // Unit test will be minimal because the producer plugin is applied to an android test module,
+ // that requires a working target application. Testing will be covered only by integration tests.
+
+ private val rootFolder = TemporaryFolder().also { it.create() }
+
+ @get:Rule
+ val producerProjectSetup = ProjectSetupRule(rootFolder.root)
+
+ @get:Rule
+ val buildProviderProjectSetup = ProjectSetupRule(rootFolder.root)
+
+ private lateinit var producerModuleName: String
+ private lateinit var buildProviderModuleName: String
+ private lateinit var gradleRunner: GradleRunner
+
+ @Before
+ fun setUp() {
+ producerModuleName = producerProjectSetup.rootDir.relativeTo(rootFolder.root).name
+ buildProviderModuleName = buildProviderProjectSetup.rootDir.relativeTo(rootFolder.root).name
+
+ rootFolder.newFile("settings.gradle").writeText(
+ """
+ include '$producerModuleName'
+ include '$buildProviderModuleName'
+ """.trimIndent()
+ )
+ gradleRunner = GradleRunner.create()
+ .withProjectDir(producerProjectSetup.rootDir)
+ .withPluginClasspath()
+ }
+
+ @Test
+ fun verifyTasksWithAndroidTestPlugin() {
+ buildProviderProjectSetup.writeDefaultBuildGradle(
+ prefix = """
+ plugins {
+ id("com.android.application")
+ id("androidx.baselineprofiles.buildprovider")
+ }
+ android {
+ namespace 'com.example.namespace'
+ }
+ """.trimIndent(),
+ suffix = ""
+ )
+ producerProjectSetup.writeDefaultBuildGradle(
+ prefix = """
+ plugins {
+ id("com.android.test")
+ id("androidx.baselineprofiles.producer")
+ }
+ android {
+ targetProjectPath = ":$buildProviderModuleName"
+ namespace 'com.example.namespace.test'
+ }
+ tasks.register("mergeNonObfuscatedReleaseTestResultProtos") { println("Stub") }
+ """.trimIndent(),
+ suffix = ""
+ )
+
+ val output = gradleRunner.withArguments("tasks", "--stacktrace").build().output
+ assertTrue { output.contains("collectNonObfuscatedReleaseBaselineProfiles - ") }
+ }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
index 936c42f..ce57b42 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
@@ -117,9 +117,12 @@
project.tasks.register(
DARWIN_BENCHMARK_RESULTS_TASK, DarwinBenchmarkResultsTask::class.java
) {
+ it.group = "Verification"
+ it.description = "Run Kotlin Multiplatform Benchmarks for Darwin"
it.xcResultPath.set(runDarwinBenchmarks.flatMap { task ->
task.xcResultPath
})
+ it.referenceSha.set(extension.referenceSha)
val resultFileName = "${extension.xcodeProjectName.get()}-benchmark-result.json"
it.outputFile.set(
project.layout.buildDirectory.file(
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt
index 35fec87..ba06bad 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt
@@ -18,6 +18,7 @@
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Optional
/**
* The [DarwinBenchmarkPlugin] extension.
@@ -43,4 +44,11 @@
* This is typically discovered by using `xcrun xctrace list devices`.
*/
abstract val destination: Property<String>
+
+ /**
+ * The reference sha for the source code being benchmarked. This can be useful
+ * when tracking regressions.
+ */
+ @get:Optional
+ abstract val referenceSha: Property<String>
}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
index dfca8e9..b596566 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
@@ -25,7 +25,9 @@
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFile
@@ -42,6 +44,10 @@
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val xcResultPath: DirectoryProperty
+ @get:Input
+ @get:Optional
+ abstract val referenceSha: Property<String>
+
@get:OutputFile
abstract val outputFile: RegularFileProperty
@@ -66,7 +72,7 @@
}
}
val (record, summaries) = parser.parseResults()
- val metrics = Metrics.buildMetrics(record, summaries)
+ val metrics = Metrics.buildMetrics(record, summaries, referenceSha.orNull)
val output = GsonHelpers.gsonBuilder()
.setPrettyPrinting()
.create()
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/skia/Metric.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/skia/Metric.kt
index 6bed2de..f1c2bd8 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/skia/Metric.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/skia/Metric.kt
@@ -32,11 +32,18 @@
)
data class Metric(val key: Map<String, String>, val measurements: Measurements)
-data class Metrics(val key: Map<String, String>, val results: List<Metric>) {
+data class Metrics(
+ val key: Map<String, String>,
+ val results: List<Metric>,
+ val version: Long = 1L,
+ @SerializedName("git_hash")
+ val referenceSha: String? = null
+) {
companion object {
fun buildMetrics(
record: ActionsInvocationRecord,
- summaries: List<ActionTestSummary>
+ summaries: List<ActionTestSummary>,
+ referenceSha: String?,
): Metrics {
require(record.actions.actionRecords.isNotEmpty())
val runDestination = record.actions.actionRecords.first().runDestination
@@ -49,7 +56,7 @@
"modelCode" to runDestination.localComputerRecord.modelCode.value
)
val results = summaries.flatMap { it.toMetrics() }
- return Metrics(metricsKeys, results)
+ return Metrics(metricsKeys, results, referenceSha = referenceSha)
}
private fun ActionTestSummary.toMetrics(): List<Metric> {
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt
index 2ce55a7..dc9d985 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt
@@ -50,7 +50,7 @@
// Metrics typically correspond to the number of tests
assertThat(record.metrics.size()).isEqualTo(2)
assertThat(summaries.isNotEmpty()).isTrue()
- val metrics = Metrics.buildMetrics(record, summaries)
+ val metrics = Metrics.buildMetrics(record, summaries, referenceSha = null)
val json = GsonHelpers.gsonBuilder()
.setPrettyPrinting()
.create()
diff --git a/benchmark/benchmark-darwin-samples/build.gradle b/benchmark/benchmark-darwin-samples/build.gradle
index c946afe..c297722 100644
--- a/benchmark/benchmark-darwin-samples/build.gradle
+++ b/benchmark/benchmark-darwin-samples/build.gradle
@@ -56,6 +56,7 @@
scheme = "testapp-ios"
// ios 13, 15.2
destination = "platform=iOS Simulator,name=iPhone 13,OS=15.2"
+ referenceSha.set(androidx.getReferenceSha())
}
androidx {
diff --git a/benchmark/integration-tests/baselineprofiles-consumer/build.gradle b/benchmark/integration-tests/baselineprofiles-consumer/build.gradle
new file mode 100644
index 0000000..d3e1e45
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-consumer/build.gradle
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.application")
+ id("kotlin-android")
+ id("androidx.baselineprofiles.consumer")
+ id("androidx.baselineprofiles.buildprovider")
+}
+
+android {
+ buildTypes {
+ release {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile("proguard-android-optimize.txt")
+ }
+ }
+ namespace "androidx.benchmark.integration.baselineprofiles.consumer"
+}
+
+dependencies {
+ implementation(libs.kotlinStdlib)
+ implementation(libs.constraintLayout)
+ baselineprofiles(project(":benchmark:integration-tests:baselineprofiles-producer"))
+}
+
+apply(from: "../baselineprofiles-test-utils/utils.gradle")
diff --git a/benchmark/integration-tests/baselineprofiles-consumer/src/main/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-consumer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ca9827a
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-consumer/src/main/AndroidManifest.xml
@@ -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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <application
+ android:allowBackup="false"
+ android:label="Jetpack Baselineprofiles Target"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.AppCompat"
+ tools:ignore="MissingApplicationIcon">
+
+ <activity
+ android:name=".EmptyActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="androidx.benchmark.integration.baselineprofiles.consumer.EMPTY_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <profileable android:shell="true" />
+ </application>
+</manifest>
diff --git a/benchmark/integration-tests/baselineprofiles-consumer/src/main/expected-baseline-prof.txt b/benchmark/integration-tests/baselineprofiles-consumer/src/main/expected-baseline-prof.txt
new file mode 100644
index 0000000..205f8c9
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-consumer/src/main/expected-baseline-prof.txt
@@ -0,0 +1,172 @@
+HSPLandroidx/appcompat/widget/TintTypedArray;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/appcompat/widget/TintTypedArray;->measure(Landroidx/constraintlayout/widget/ConstraintLayout$Measurer;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Z)Z
+HSPLandroidx/appcompat/widget/TintTypedArray;->solveLinearSystem(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;II)V
+HSPLandroidx/benchmark/integration/baselineprofiles/consumer/EmptyActivity;-><init>()V
+HSPLandroidx/benchmark/integration/baselineprofiles/consumer/EmptyActivity;->onCreate(Landroid/os/Bundle;)V
+HSPLandroidx/collection/ArrayMap$1;-><init>()V
+HSPLandroidx/constraintlayout/solver/ArrayLinkedVariables;-><init>(Landroidx/constraintlayout/solver/ArrayRow;Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;-><init>()V
+HSPLandroidx/constraintlayout/solver/ArrayRow;-><init>(Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->addError(Landroidx/constraintlayout/solver/LinearSystem;I)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowGreaterThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowLowerThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->pivot(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->reset()V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->updateFromFinalVariable(Landroidx/constraintlayout/solver/SolverVariable;Z)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->updateFromRow(Landroidx/constraintlayout/solver/ArrayRow;Z)V
+HSPLandroidx/constraintlayout/solver/LinearSystem$ValuesRow;-><init>(Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;-><init>()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->acquireSolverVariable$enumunboxing$(I)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addCentering(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;IFLandroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addConstraint(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addEquality(Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addEquality(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addGreaterThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addLowerThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createErrorVariable(I)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createObjectVariable(Ljava/lang/Object;)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createRow()Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createSlackVariable()Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->getObjectVariableValue(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;)I
+HSPLandroidx/constraintlayout/solver/LinearSystem;->increaseTableSize()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->optimize(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->releaseRows()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->reset()V
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;-><init>()V
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;->acquire()Ljava/lang/Object;
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;->release(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;-><init>(Landroidx/constraintlayout/solver/PriorityGoalRow;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;-><init>(Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->addToGoal(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->getPivotCandidate([Z)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->removeGoal(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->updateFromRow(Landroidx/constraintlayout/solver/ArrayRow;Z)V
+HSPLandroidx/constraintlayout/solver/SolverVariable$Type$EnumUnboxingSharedUtility;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/SolverVariable$Type$EnumUnboxingSharedUtility;->ordinal(I)I
+HSPLandroidx/constraintlayout/solver/SolverVariable;-><init>(I)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->addToRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->removeFromRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->reset()V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->updateReferencesWithNewDefinition(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;-><init>(Landroidx/constraintlayout/solver/ArrayRow;Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->add(Landroidx/constraintlayout/solver/SolverVariable;FZ)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->addToHashMap(Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->addVariable(ILandroidx/constraintlayout/solver/SolverVariable;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->clear()V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->divideByAmount(F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->get(Landroidx/constraintlayout/solver/SolverVariable;)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getCurrentSize()I
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getVariable(I)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getVariableValue(I)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->indexOf(Landroidx/constraintlayout/solver/SolverVariable;)I
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->invert()V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->put(Landroidx/constraintlayout/solver/SolverVariable;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->remove(Landroidx/constraintlayout/solver/SolverVariable;Z)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->use(Landroidx/constraintlayout/solver/ArrayRow;Z)F
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;-><init>(ILjava/lang/String;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->connect(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;II)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->getMargin()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->isConnected()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->reset()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->resetSolverVariable()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->addToSolver(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->applyConstraints$enumunboxing$(Landroidx/constraintlayout/solver/LinearSystem;ZZZZLandroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;IZLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;IIIIFZZZZIIIIFZ)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->createObjectVariables(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getAnchor(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;)Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getDimensionBehaviour$enumunboxing$(I)I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getHeight()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getWidth()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getX()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getY()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->immediateConnect(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;II)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isChainHead(I)Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isInHorizontalChain()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isInVerticalChain()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->reset()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->resetSolverVariables(Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHeight(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalDimensionBehaviour$enumunboxing$(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalDimensionBehaviour$enumunboxing$(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setWidth(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->updateFromSolver(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->addChildrenToSolver(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->layout()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->resetSolverVariables(Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyNode;-><init>(Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DimensionDependency;-><init>(Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/VerticalWidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams$Table;-><clinit>()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;->resolveLayoutDirection(I)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;->validate()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;-><init>(Landroidx/constraintlayout/widget/ConstraintLayout;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;->measure(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->addView(Landroid/view/View;ILandroid/view/ViewGroup$LayoutParams;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->applyConstraintsFromLayoutParams(ZLandroid/view/View;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;Landroid/util/SparseArray;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->checkLayoutParams(Landroid/view/ViewGroup$LayoutParams;)Z
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->dispatchDraw(Landroid/graphics/Canvas;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->generateLayoutParams(Landroid/util/AttributeSet;)Landroid/view/ViewGroup$LayoutParams;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->getPaddingWidth()I
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->getViewWidget(Landroid/view/View;)Landroidx/constraintlayout/solver/widgets/ConstraintWidget;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->isRtl()Z
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onLayout(ZIIII)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onMeasure(II)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onViewAdded(Landroid/view/View;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->requestLayout()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->updateHierarchy()Z
+HSPLandroidx/constraintlayout/widget/R$styleable;-><clinit>()V
+HSPLandroidx/core/app/CoreComponentFactory;-><init>()V
+HSPLandroidx/core/app/CoreComponentFactory;->instantiateActivity(Ljava/lang/ClassLoader;Ljava/lang/String;Landroid/content/Intent;)Landroid/app/Activity;
+HSPLandroidx/core/app/CoreComponentFactory;->instantiateApplication(Ljava/lang/ClassLoader;Ljava/lang/String;)Landroid/app/Application;
+HSPLandroidx/core/util/ObjectsCompat;-><clinit>()V
+Landroidx/appcompat/widget/TintTypedArray;
+Landroidx/benchmark/integration/baselineprofiles/consumer/EmptyActivity;
+Landroidx/collection/ArrayMap$1;
+Landroidx/constraintlayout/solver/ArrayLinkedVariables;
+Landroidx/constraintlayout/solver/ArrayRow$ArrayRowVariables;
+Landroidx/constraintlayout/solver/ArrayRow;
+Landroidx/constraintlayout/solver/LinearSystem$ValuesRow;
+Landroidx/constraintlayout/solver/LinearSystem;
+Landroidx/constraintlayout/solver/Pools$SimplePool;
+Landroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;
+Landroidx/constraintlayout/solver/PriorityGoalRow;
+Landroidx/constraintlayout/solver/SolverVariable$Type$EnumUnboxingSharedUtility;
+Landroidx/constraintlayout/solver/SolverVariable;
+Landroidx/constraintlayout/solver/SolverVariableValues;
+Landroidx/constraintlayout/solver/widgets/Barrier;
+Landroidx/constraintlayout/solver/widgets/ChainHead;
+Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;
+Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidget;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;
+Landroidx/constraintlayout/solver/widgets/Guideline;
+Landroidx/constraintlayout/solver/widgets/Helper;
+Landroidx/constraintlayout/solver/widgets/HelperWidget;
+Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;
+Landroidx/constraintlayout/solver/widgets/analyzer/Dependency;
+Landroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;
+Landroidx/constraintlayout/solver/widgets/analyzer/DependencyNode;
+Landroidx/constraintlayout/solver/widgets/analyzer/DimensionDependency;
+Landroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;
+Landroidx/constraintlayout/solver/widgets/analyzer/VerticalWidgetRun;
+Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;
+Landroidx/constraintlayout/widget/ConstraintHelper;
+Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams$Table;
+Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;
+Landroidx/constraintlayout/widget/ConstraintLayout$Measurer;
+Landroidx/constraintlayout/widget/ConstraintLayout;
+Landroidx/constraintlayout/widget/Guideline;
+Landroidx/constraintlayout/widget/R$styleable;
+Landroidx/core/app/CoreComponentFactory;
+Landroidx/core/util/ObjectsCompat;
\ No newline at end of file
diff --git a/benchmark/integration-tests/baselineprofiles-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/consumer/EmptyActivity.kt b/benchmark/integration-tests/baselineprofiles-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/consumer/EmptyActivity.kt
new file mode 100644
index 0000000..9a1b40b
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/consumer/EmptyActivity.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.
+ */
+
+package androidx.benchmark.integration.baselineprofiles.consumer
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+
+class EmptyActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ findViewById<TextView>(R.id.txtNotice).setText(R.string.app_notice)
+ }
+}
diff --git a/benchmark/integration-tests/baselineprofiles-consumer/src/main/res/layout/activity_main.xml b/benchmark/integration-tests/baselineprofiles-consumer/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..7739482
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-consumer/src/main/res/layout/activity_main.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ 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.
+-->
+
+<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">
+
+ <TextView
+ android:id="@+id/txtNotice"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Preview Some Text" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/benchmark/integration-tests/baselineprofiles-consumer/src/main/res/values/donottranslate-strings.xml b/benchmark/integration-tests/baselineprofiles-consumer/src/main/res/values/donottranslate-strings.xml
new file mode 100644
index 0000000..c880dc5
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-consumer/src/main/res/values/donottranslate-strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<resources>
+ <string name="app_notice">Baseline Profiles Integration Test App.</string>
+</resources>
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-consumer/build.gradle b/benchmark/integration-tests/baselineprofiles-flavors-consumer/build.gradle
new file mode 100644
index 0000000..8ae2e3f
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-consumer/build.gradle
@@ -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.
+ */
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.application")
+ id("kotlin-android")
+ id("androidx.baselineprofiles.buildprovider")
+ id("androidx.baselineprofiles.consumer")
+}
+
+android {
+ buildTypes {
+ release {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile("proguard-android-optimize.txt")
+ }
+ }
+ productFlavors {
+ flavorDimensions = ["version"]
+ free {
+ dimension "version"
+ applicationIdSuffix ".free"
+ versionNameSuffix "-free"
+ }
+ paid {
+ dimension "version"
+ applicationIdSuffix ".paid"
+ versionNameSuffix "-paid"
+ }
+ }
+ namespace "androidx.benchmark.integration.baselineprofiles.flavors.consumer"
+}
+
+dependencies {
+ implementation(libs.kotlinStdlib)
+ implementation(libs.constraintLayout)
+
+ baselineprofiles(project(":benchmark:integration-tests:baselineprofiles-flavors-producer"))
+}
+
+baselineProfilesProfileConsumer {
+ buildTypeName = "release"
+}
+
+apply(from: "../baselineprofiles-test-utils/utils.gradle")
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/free/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/free/AndroidManifest.xml
new file mode 100644
index 0000000..fb4bcd8
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/free/AndroidManifest.xml
@@ -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.
+ -->
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:node="merge">
+
+ <application
+ android:allowBackup="false"
+ android:label="Jetpack Baselineprofiles Target"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.AppCompat"
+ tools:ignore="MissingApplicationIcon">
+
+ <activity
+ android:name=".EmptyActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="androidx.benchmark.integration.baselineprofiles.flavors.consumer.free.EMPTY_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <profileable android:shell="true" />
+ </application>
+</manifest>
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..118910a
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<!--
+ ~ 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.
+ -->
+<manifest
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:node="merge">
+
+ <application />
+</manifest>
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/expected-baseline-prof.txt b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/expected-baseline-prof.txt
new file mode 100644
index 0000000..49756602
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/expected-baseline-prof.txt
@@ -0,0 +1,172 @@
+HSPLandroidx/appcompat/widget/TintTypedArray;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/appcompat/widget/TintTypedArray;->measure(Landroidx/constraintlayout/widget/ConstraintLayout$Measurer;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Z)Z
+HSPLandroidx/appcompat/widget/TintTypedArray;->solveLinearSystem(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;II)V
+HSPLandroidx/benchmark/integration/baselineprofiles/flavors/consumer/EmptyActivity;-><init>()V
+HSPLandroidx/benchmark/integration/baselineprofiles/flavors/consumer/EmptyActivity;->onCreate(Landroid/os/Bundle;)V
+HSPLandroidx/collection/ArrayMap$1;-><init>()V
+HSPLandroidx/constraintlayout/solver/ArrayLinkedVariables;-><init>(Landroidx/constraintlayout/solver/ArrayRow;Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;-><init>()V
+HSPLandroidx/constraintlayout/solver/ArrayRow;-><init>(Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->addError(Landroidx/constraintlayout/solver/LinearSystem;I)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowGreaterThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowLowerThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->pivot(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->reset()V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->updateFromFinalVariable(Landroidx/constraintlayout/solver/SolverVariable;Z)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->updateFromRow(Landroidx/constraintlayout/solver/ArrayRow;Z)V
+HSPLandroidx/constraintlayout/solver/LinearSystem$ValuesRow;-><init>(Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;-><init>()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->acquireSolverVariable$enumunboxing$(I)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addCentering(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;IFLandroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addConstraint(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addEquality(Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addEquality(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addGreaterThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addLowerThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createErrorVariable(I)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createObjectVariable(Ljava/lang/Object;)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createRow()Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createSlackVariable()Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->getObjectVariableValue(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;)I
+HSPLandroidx/constraintlayout/solver/LinearSystem;->increaseTableSize()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->optimize(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->releaseRows()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->reset()V
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;-><init>()V
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;->acquire()Ljava/lang/Object;
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;->release(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;-><init>(Landroidx/constraintlayout/solver/PriorityGoalRow;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;-><init>(Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->addToGoal(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->getPivotCandidate([Z)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->removeGoal(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->updateFromRow(Landroidx/constraintlayout/solver/ArrayRow;Z)V
+HSPLandroidx/constraintlayout/solver/SolverVariable$Type$EnumUnboxingSharedUtility;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/SolverVariable$Type$EnumUnboxingSharedUtility;->ordinal(I)I
+HSPLandroidx/constraintlayout/solver/SolverVariable;-><init>(I)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->addToRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->removeFromRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->reset()V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->updateReferencesWithNewDefinition(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;-><init>(Landroidx/constraintlayout/solver/ArrayRow;Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->add(Landroidx/constraintlayout/solver/SolverVariable;FZ)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->addToHashMap(Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->addVariable(ILandroidx/constraintlayout/solver/SolverVariable;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->clear()V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->divideByAmount(F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->get(Landroidx/constraintlayout/solver/SolverVariable;)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getCurrentSize()I
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getVariable(I)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getVariableValue(I)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->indexOf(Landroidx/constraintlayout/solver/SolverVariable;)I
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->invert()V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->put(Landroidx/constraintlayout/solver/SolverVariable;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->remove(Landroidx/constraintlayout/solver/SolverVariable;Z)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->use(Landroidx/constraintlayout/solver/ArrayRow;Z)F
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;-><init>(ILjava/lang/String;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->connect(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;II)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->getMargin()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->isConnected()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->reset()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->resetSolverVariable()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->addToSolver(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->applyConstraints$enumunboxing$(Landroidx/constraintlayout/solver/LinearSystem;ZZZZLandroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;IZLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;IIIIFZZZZIIIIFZ)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->createObjectVariables(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getAnchor(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;)Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getDimensionBehaviour$enumunboxing$(I)I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getHeight()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getWidth()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getX()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getY()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->immediateConnect(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;II)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isChainHead(I)Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isInHorizontalChain()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isInVerticalChain()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->reset()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->resetSolverVariables(Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHeight(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalDimensionBehaviour$enumunboxing$(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalDimensionBehaviour$enumunboxing$(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setWidth(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->updateFromSolver(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->addChildrenToSolver(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->layout()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->resetSolverVariables(Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyNode;-><init>(Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DimensionDependency;-><init>(Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/VerticalWidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams$Table;-><clinit>()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;->resolveLayoutDirection(I)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;->validate()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;-><init>(Landroidx/constraintlayout/widget/ConstraintLayout;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;->measure(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->addView(Landroid/view/View;ILandroid/view/ViewGroup$LayoutParams;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->applyConstraintsFromLayoutParams(ZLandroid/view/View;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;Landroid/util/SparseArray;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->checkLayoutParams(Landroid/view/ViewGroup$LayoutParams;)Z
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->dispatchDraw(Landroid/graphics/Canvas;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->generateLayoutParams(Landroid/util/AttributeSet;)Landroid/view/ViewGroup$LayoutParams;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->getPaddingWidth()I
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->getViewWidget(Landroid/view/View;)Landroidx/constraintlayout/solver/widgets/ConstraintWidget;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->isRtl()Z
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onLayout(ZIIII)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onMeasure(II)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onViewAdded(Landroid/view/View;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->requestLayout()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->updateHierarchy()Z
+HSPLandroidx/constraintlayout/widget/R$styleable;-><clinit>()V
+HSPLandroidx/core/app/CoreComponentFactory;-><init>()V
+HSPLandroidx/core/app/CoreComponentFactory;->instantiateActivity(Ljava/lang/ClassLoader;Ljava/lang/String;Landroid/content/Intent;)Landroid/app/Activity;
+HSPLandroidx/core/app/CoreComponentFactory;->instantiateApplication(Ljava/lang/ClassLoader;Ljava/lang/String;)Landroid/app/Application;
+HSPLandroidx/core/util/ObjectsCompat;-><clinit>()V
+Landroidx/appcompat/widget/TintTypedArray;
+Landroidx/benchmark/integration/baselineprofiles/flavors/consumer/EmptyActivity;
+Landroidx/collection/ArrayMap$1;
+Landroidx/constraintlayout/solver/ArrayLinkedVariables;
+Landroidx/constraintlayout/solver/ArrayRow$ArrayRowVariables;
+Landroidx/constraintlayout/solver/ArrayRow;
+Landroidx/constraintlayout/solver/LinearSystem$ValuesRow;
+Landroidx/constraintlayout/solver/LinearSystem;
+Landroidx/constraintlayout/solver/Pools$SimplePool;
+Landroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;
+Landroidx/constraintlayout/solver/PriorityGoalRow;
+Landroidx/constraintlayout/solver/SolverVariable$Type$EnumUnboxingSharedUtility;
+Landroidx/constraintlayout/solver/SolverVariable;
+Landroidx/constraintlayout/solver/SolverVariableValues;
+Landroidx/constraintlayout/solver/widgets/Barrier;
+Landroidx/constraintlayout/solver/widgets/ChainHead;
+Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;
+Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidget;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;
+Landroidx/constraintlayout/solver/widgets/Guideline;
+Landroidx/constraintlayout/solver/widgets/Helper;
+Landroidx/constraintlayout/solver/widgets/HelperWidget;
+Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;
+Landroidx/constraintlayout/solver/widgets/analyzer/Dependency;
+Landroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;
+Landroidx/constraintlayout/solver/widgets/analyzer/DependencyNode;
+Landroidx/constraintlayout/solver/widgets/analyzer/DimensionDependency;
+Landroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;
+Landroidx/constraintlayout/solver/widgets/analyzer/VerticalWidgetRun;
+Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;
+Landroidx/constraintlayout/widget/ConstraintHelper;
+Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams$Table;
+Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;
+Landroidx/constraintlayout/widget/ConstraintLayout$Measurer;
+Landroidx/constraintlayout/widget/ConstraintLayout;
+Landroidx/constraintlayout/widget/Guideline;
+Landroidx/constraintlayout/widget/R$styleable;
+Landroidx/core/app/CoreComponentFactory;
+Landroidx/core/util/ObjectsCompat;
\ No newline at end of file
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/flavors/consumer/EmptyActivity.kt b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/flavors/consumer/EmptyActivity.kt
new file mode 100644
index 0000000..5119859
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/flavors/consumer/EmptyActivity.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.
+ */
+
+package androidx.benchmark.integration.baselineprofiles.flavors.consumer
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+
+class EmptyActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ findViewById<TextView>(R.id.txtNotice).setText(R.string.app_notice)
+ }
+}
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/res/layout/activity_main.xml b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..7739482
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/res/layout/activity_main.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ 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.
+-->
+
+<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">
+
+ <TextView
+ android:id="@+id/txtNotice"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Preview Some Text" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/res/values/donottranslate-strings.xml b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/res/values/donottranslate-strings.xml
new file mode 100644
index 0000000..c880dc5
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/main/res/values/donottranslate-strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<resources>
+ <string name="app_notice">Baseline Profiles Integration Test App.</string>
+</resources>
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/paid/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/paid/AndroidManifest.xml
new file mode 100644
index 0000000..514b52c
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-consumer/src/paid/AndroidManifest.xml
@@ -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.
+ -->
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:node="merge">
+
+ <application
+ android:allowBackup="false"
+ android:label="Jetpack Baselineprofiles Target"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.AppCompat"
+ tools:ignore="MissingApplicationIcon">
+
+ <activity
+ android:name=".EmptyActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="androidx.benchmark.integration.baselineprofiles.flavors.consumer.paid.EMPTY_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <profileable android:shell="true" />
+ </application>
+</manifest>
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-producer/build.gradle b/benchmark/integration-tests/baselineprofiles-flavors-producer/build.gradle
new file mode 100644
index 0000000..d3e6cdb
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-producer/build.gradle
@@ -0,0 +1,68 @@
+import com.android.build.api.dsl.ManagedVirtualDevice
+
+/*
+ * 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.
+ */
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.test")
+ id("kotlin-android")
+ id("androidx.baselineprofiles.producer")
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 23
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ testOptions.managedDevices.devices {
+ pixel6Api31(ManagedVirtualDevice) {
+ device = "Pixel 6"
+ apiLevel = 31
+ systemImageSource = "aosp"
+ }
+ }
+ buildTypes {
+ release { }
+ }
+ productFlavors {
+ flavorDimensions = ["version"]
+ free { dimension "version" }
+ paid { dimension "version" }
+ }
+ targetProjectPath = ":benchmark:integration-tests:baselineprofiles-flavors-consumer"
+ namespace "androidx.benchmark.integration.baselineprofiles.flavors.producer"
+}
+
+dependencies {
+ implementation(project(":benchmark:benchmark-junit4"))
+ implementation(project(":benchmark:benchmark-macro-junit4"))
+ implementation(libs.testRules)
+ implementation(libs.testExtJunit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
+ implementation(libs.testUiautomator)
+ implementation(libs.testExtTruth)
+}
+
+baselineProfilesProfileProducer {
+ managedDevices += "pixel6Api31"
+ useConnectedDevices = false
+}
+
+androidx {
+ disableDeviceTests = true
+}
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-producer/src/free/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/free/AndroidManifest.xml
new file mode 100644
index 0000000..bae036b
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/free/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<manifest />
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-producer/src/free/java/androidx/benchmark/integration/baselineprofiles/flavors/producer/BaselineProfileTest.kt b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/free/java/androidx/benchmark/integration/baselineprofiles/flavors/producer/BaselineProfileTest.kt
new file mode 100644
index 0000000..1fd878f
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/free/java/androidx/benchmark/integration/baselineprofiles/flavors/producer/BaselineProfileTest.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.benchmark.integration.baselineprofiles.flavors.producer
+
+import android.content.Intent
+import android.os.Build
+import androidx.benchmark.DeviceInfo
+import androidx.benchmark.macro.junit4.BaselineProfileRule
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 29)
+class BaselineProfileTest {
+
+ @get:Rule
+ val baselineRule = BaselineProfileRule()
+
+ @Test
+ fun startupBaselineProfile() {
+ assumeTrue(DeviceInfo.isRooted || Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
+
+ // Collects the baseline profile
+ baselineRule.collectBaselineProfile(
+ packageName = PACKAGE_NAME,
+ profileBlock = {
+ startActivityAndWait(Intent(ACTION))
+ device.waitForIdle()
+ }
+ )
+ }
+
+ companion object {
+ private const val PACKAGE_NAME =
+ "androidx.benchmark.integration.baselineprofiles.flavors.consumer.free"
+ private const val ACTION =
+ "androidx.benchmark.integration.baselineprofiles.flavors.consumer.free.EMPTY_ACTIVITY"
+ }
+}
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-producer/src/main/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d4c1970
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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.
+ -->
+<manifest />
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-producer/src/paid/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/paid/AndroidManifest.xml
new file mode 100644
index 0000000..bae036b
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/paid/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<manifest />
diff --git a/benchmark/integration-tests/baselineprofiles-flavors-producer/src/paid/java/androidx/benchmark/integration/baselineprofiles/flavors/producer/BaselineProfileTest.kt b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/paid/java/androidx/benchmark/integration/baselineprofiles/flavors/producer/BaselineProfileTest.kt
new file mode 100644
index 0000000..6a96947
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-flavors-producer/src/paid/java/androidx/benchmark/integration/baselineprofiles/flavors/producer/BaselineProfileTest.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.benchmark.integration.baselineprofiles.flavors.producer
+
+import android.content.Intent
+import android.os.Build
+import androidx.benchmark.DeviceInfo
+import androidx.benchmark.macro.junit4.BaselineProfileRule
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 29)
+class BaselineProfileTest {
+
+ @get:Rule
+ val baselineRule = BaselineProfileRule()
+
+ @Test
+ fun startupBaselineProfile() {
+ assumeTrue(DeviceInfo.isRooted || Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
+
+ // Collects the baseline profile
+ baselineRule.collectBaselineProfile(
+ packageName = PACKAGE_NAME,
+ profileBlock = {
+ startActivityAndWait(Intent(ACTION))
+ device.waitForIdle()
+ }
+ )
+ }
+
+ companion object {
+ private const val PACKAGE_NAME =
+ "androidx.benchmark.integration.baselineprofiles.flavors.consumer.paid"
+ private const val ACTION =
+ "androidx.benchmark.integration.baselineprofiles.flavors.consumer.paid.EMPTY_ACTIVITY"
+ }
+}
diff --git a/benchmark/integration-tests/baselineprofiles-library-build-provider/build.gradle b/benchmark/integration-tests/baselineprofiles-library-build-provider/build.gradle
new file mode 100644
index 0000000..9c8061f
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-build-provider/build.gradle
@@ -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.
+ */
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.application")
+ id("kotlin-android")
+ id("androidx.baselineprofiles.buildprovider")
+}
+
+android {
+ buildTypes {
+ release {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile("proguard-android-optimize.txt")
+ }
+ }
+ namespace "androidx.benchmark.integration.baselineprofiles.library.buildprovider"
+}
+
+dependencies {
+ implementation(libs.kotlinStdlib)
+ implementation(libs.constraintLayout)
+}
diff --git a/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..fbb0bc7
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/AndroidManifest.xml
@@ -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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <application
+ android:allowBackup="false"
+ android:label="Jetpack Baselineprofiles Target"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.AppCompat"
+ tools:ignore="MissingApplicationIcon">
+
+ <activity
+ android:name=".EmptyActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="androidx.benchmark.integration.baselineprofiles.EMPTY_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <profileable android:shell="true" />
+ </application>
+</manifest>
diff --git a/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/java/androidx/benchmark/integration/baselineprofiles/library/buildprovider/EmptyActivity.kt b/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/java/androidx/benchmark/integration/baselineprofiles/library/buildprovider/EmptyActivity.kt
new file mode 100644
index 0000000..64b8ed7
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/java/androidx/benchmark/integration/baselineprofiles/library/buildprovider/EmptyActivity.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.
+ */
+
+package androidx.benchmark.integration.baselineprofiles.library.buildprovider
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+
+class EmptyActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ findViewById<TextView>(R.id.txtNotice).setText(R.string.app_notice)
+ }
+}
diff --git a/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/res/layout/activity_main.xml b/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..7739482
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/res/layout/activity_main.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ 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.
+-->
+
+<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">
+
+ <TextView
+ android:id="@+id/txtNotice"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Preview Some Text" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/res/values/donottranslate-strings.xml b/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/res/values/donottranslate-strings.xml
new file mode 100644
index 0000000..c880dc5
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-build-provider/src/main/res/values/donottranslate-strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<resources>
+ <string name="app_notice">Baseline Profiles Integration Test App.</string>
+</resources>
diff --git a/benchmark/integration-tests/baselineprofiles-library-consumer/build.gradle b/benchmark/integration-tests/baselineprofiles-library-consumer/build.gradle
new file mode 100644
index 0000000..e314c41
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-consumer/build.gradle
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("kotlin-android")
+ id("androidx.baselineprofiles.consumer")
+}
+
+android {
+ namespace "androidx.benchmark.integration.baselineprofiles.library.consumer"
+}
+
+dependencies {
+ baselineprofiles(project(":benchmark:integration-tests:baselineprofiles-library-producer"))
+}
+
+dependencies {
+ implementation(libs.kotlinStdlib)
+}
+
+apply(from: "../baselineprofiles-test-utils/utils.gradle")
diff --git a/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..7c52910
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+ ~ 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.
+ -->
+<manifest />
diff --git a/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/expected-baseline-prof.txt b/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/expected-baseline-prof.txt
new file mode 100644
index 0000000..29e1de4
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/expected-baseline-prof.txt
@@ -0,0 +1,172 @@
+HSPLandroidx/appcompat/widget/TintTypedArray;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/appcompat/widget/TintTypedArray;->measure(Landroidx/constraintlayout/widget/ConstraintLayout$Measurer;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Z)Z
+HSPLandroidx/appcompat/widget/TintTypedArray;->solveLinearSystem(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;II)V
+HSPLandroidx/benchmark/integration/baselineprofiles/library/buildprovider/EmptyActivity;-><init>()V
+HSPLandroidx/benchmark/integration/baselineprofiles/library/buildprovider/EmptyActivity;->onCreate(Landroid/os/Bundle;)V
+HSPLandroidx/collection/ArrayMap$1;-><init>()V
+HSPLandroidx/constraintlayout/solver/ArrayLinkedVariables;-><init>(Landroidx/constraintlayout/solver/ArrayRow;Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;-><init>()V
+HSPLandroidx/constraintlayout/solver/ArrayRow;-><init>(Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->addError(Landroidx/constraintlayout/solver/LinearSystem;I)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowGreaterThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->createRowLowerThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->pivot(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->reset()V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->updateFromFinalVariable(Landroidx/constraintlayout/solver/SolverVariable;Z)V
+HSPLandroidx/constraintlayout/solver/ArrayRow;->updateFromRow(Landroidx/constraintlayout/solver/ArrayRow;Z)V
+HSPLandroidx/constraintlayout/solver/LinearSystem$ValuesRow;-><init>(Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;-><init>()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->acquireSolverVariable$enumunboxing$(I)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addCentering(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;IFLandroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addConstraint(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addEquality(Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addEquality(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addGreaterThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addLowerThan(Landroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;II)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->addRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createErrorVariable(I)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createObjectVariable(Ljava/lang/Object;)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createRow()Landroidx/constraintlayout/solver/ArrayRow;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->createSlackVariable()Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/LinearSystem;->getObjectVariableValue(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;)I
+HSPLandroidx/constraintlayout/solver/LinearSystem;->increaseTableSize()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->optimize(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->releaseRows()V
+HSPLandroidx/constraintlayout/solver/LinearSystem;->reset()V
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;-><init>()V
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;->acquire()Ljava/lang/Object;
+HSPLandroidx/constraintlayout/solver/Pools$SimplePool;->release(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;-><init>(Landroidx/constraintlayout/solver/PriorityGoalRow;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;-><init>(Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->addToGoal(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->getPivotCandidate([Z)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->removeGoal(Landroidx/constraintlayout/solver/SolverVariable;)V
+HSPLandroidx/constraintlayout/solver/PriorityGoalRow;->updateFromRow(Landroidx/constraintlayout/solver/ArrayRow;Z)V
+HSPLandroidx/constraintlayout/solver/SolverVariable$Type$EnumUnboxingSharedUtility;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/SolverVariable$Type$EnumUnboxingSharedUtility;->ordinal(I)I
+HSPLandroidx/constraintlayout/solver/SolverVariable;-><init>(I)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->addToRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->removeFromRow(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->reset()V
+HSPLandroidx/constraintlayout/solver/SolverVariable;->updateReferencesWithNewDefinition(Landroidx/constraintlayout/solver/ArrayRow;)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;-><init>(Landroidx/constraintlayout/solver/ArrayRow;Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->add(Landroidx/constraintlayout/solver/SolverVariable;FZ)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->addToHashMap(Landroidx/constraintlayout/solver/SolverVariable;I)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->addVariable(ILandroidx/constraintlayout/solver/SolverVariable;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->clear()V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->divideByAmount(F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->get(Landroidx/constraintlayout/solver/SolverVariable;)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getCurrentSize()I
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getVariable(I)Landroidx/constraintlayout/solver/SolverVariable;
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->getVariableValue(I)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->indexOf(Landroidx/constraintlayout/solver/SolverVariable;)I
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->invert()V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->put(Landroidx/constraintlayout/solver/SolverVariable;F)V
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->remove(Landroidx/constraintlayout/solver/SolverVariable;Z)F
+HSPLandroidx/constraintlayout/solver/SolverVariableValues;->use(Landroidx/constraintlayout/solver/ArrayRow;Z)F
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;-><init>(ILjava/lang/String;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->connect(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;II)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->getMargin()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->isConnected()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->reset()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;->resetSolverVariable()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->addToSolver(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->applyConstraints$enumunboxing$(Landroidx/constraintlayout/solver/LinearSystem;ZZZZLandroidx/constraintlayout/solver/SolverVariable;Landroidx/constraintlayout/solver/SolverVariable;IZLandroidx/constraintlayout/solver/widgets/ConstraintAnchor;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;IIIIFZZZZIIIIFZ)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->createObjectVariables(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getAnchor(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;)Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getDimensionBehaviour$enumunboxing$(I)I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getHeight()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getWidth()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getX()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->getY()I
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->immediateConnect(Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;II)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isChainHead(I)Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isInHorizontalChain()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->isInVerticalChain()Z
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->reset()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->resetSolverVariables(Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHeight(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setHorizontalDimensionBehaviour$enumunboxing$(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setVerticalDimensionBehaviour$enumunboxing$(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->setWidth(I)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidget;->updateFromSolver(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->addChildrenToSolver(Landroidx/constraintlayout/solver/LinearSystem;)V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->layout()V
+HSPLandroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;->resetSolverVariables(Landroidx/collection/ArrayMap$1;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;-><init>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DependencyNode;-><init>(Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/DimensionDependency;-><init>(Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;-><clinit>()V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/VerticalWidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;-><init>(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams$Table;-><clinit>()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;->resolveLayoutDirection(I)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;->validate()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;-><init>(Landroidx/constraintlayout/widget/ConstraintLayout;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout$Measurer;->measure(Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->addView(Landroid/view/View;ILandroid/view/ViewGroup$LayoutParams;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->applyConstraintsFromLayoutParams(ZLandroid/view/View;Landroidx/constraintlayout/solver/widgets/ConstraintWidget;Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;Landroid/util/SparseArray;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->checkLayoutParams(Landroid/view/ViewGroup$LayoutParams;)Z
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->dispatchDraw(Landroid/graphics/Canvas;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->generateLayoutParams(Landroid/util/AttributeSet;)Landroid/view/ViewGroup$LayoutParams;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->getPaddingWidth()I
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->getViewWidget(Landroid/view/View;)Landroidx/constraintlayout/solver/widgets/ConstraintWidget;
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->isRtl()Z
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onLayout(ZIIII)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onMeasure(II)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->onViewAdded(Landroid/view/View;)V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->requestLayout()V
+HSPLandroidx/constraintlayout/widget/ConstraintLayout;->updateHierarchy()Z
+HSPLandroidx/constraintlayout/widget/R$styleable;-><clinit>()V
+HSPLandroidx/core/app/CoreComponentFactory;-><init>()V
+HSPLandroidx/core/app/CoreComponentFactory;->instantiateActivity(Ljava/lang/ClassLoader;Ljava/lang/String;Landroid/content/Intent;)Landroid/app/Activity;
+HSPLandroidx/core/app/CoreComponentFactory;->instantiateApplication(Ljava/lang/ClassLoader;Ljava/lang/String;)Landroid/app/Application;
+HSPLandroidx/core/util/ObjectsCompat;-><clinit>()V
+Landroidx/appcompat/widget/TintTypedArray;
+Landroidx/benchmark/integration/baselineprofiles/library/buildprovider/EmptyActivity;
+Landroidx/collection/ArrayMap$1;
+Landroidx/constraintlayout/solver/ArrayLinkedVariables;
+Landroidx/constraintlayout/solver/ArrayRow$ArrayRowVariables;
+Landroidx/constraintlayout/solver/ArrayRow;
+Landroidx/constraintlayout/solver/LinearSystem$ValuesRow;
+Landroidx/constraintlayout/solver/LinearSystem;
+Landroidx/constraintlayout/solver/Pools$SimplePool;
+Landroidx/constraintlayout/solver/PriorityGoalRow$GoalVariableAccessor;
+Landroidx/constraintlayout/solver/PriorityGoalRow;
+Landroidx/constraintlayout/solver/SolverVariable$Type$EnumUnboxingSharedUtility;
+Landroidx/constraintlayout/solver/SolverVariable;
+Landroidx/constraintlayout/solver/SolverVariableValues;
+Landroidx/constraintlayout/solver/widgets/Barrier;
+Landroidx/constraintlayout/solver/widgets/ChainHead;
+Landroidx/constraintlayout/solver/widgets/ConstraintAnchor$Type;
+Landroidx/constraintlayout/solver/widgets/ConstraintAnchor;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidget;
+Landroidx/constraintlayout/solver/widgets/ConstraintWidgetContainer;
+Landroidx/constraintlayout/solver/widgets/Guideline;
+Landroidx/constraintlayout/solver/widgets/Helper;
+Landroidx/constraintlayout/solver/widgets/HelperWidget;
+Landroidx/constraintlayout/solver/widgets/analyzer/BasicMeasure$Measure;
+Landroidx/constraintlayout/solver/widgets/analyzer/Dependency;
+Landroidx/constraintlayout/solver/widgets/analyzer/DependencyGraph;
+Landroidx/constraintlayout/solver/widgets/analyzer/DependencyNode;
+Landroidx/constraintlayout/solver/widgets/analyzer/DimensionDependency;
+Landroidx/constraintlayout/solver/widgets/analyzer/HorizontalWidgetRun;
+Landroidx/constraintlayout/solver/widgets/analyzer/VerticalWidgetRun;
+Landroidx/constraintlayout/solver/widgets/analyzer/WidgetRun;
+Landroidx/constraintlayout/widget/ConstraintHelper;
+Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams$Table;
+Landroidx/constraintlayout/widget/ConstraintLayout$LayoutParams;
+Landroidx/constraintlayout/widget/ConstraintLayout$Measurer;
+Landroidx/constraintlayout/widget/ConstraintLayout;
+Landroidx/constraintlayout/widget/Guideline;
+Landroidx/constraintlayout/widget/R$styleable;
+Landroidx/core/app/CoreComponentFactory;
+Landroidx/core/util/ObjectsCompat;
\ No newline at end of file
diff --git a/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/library/consumer/EmptyClass.kt b/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/library/consumer/EmptyClass.kt
new file mode 100644
index 0000000..47a198a
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-consumer/src/main/java/androidx/benchmark/integration/baselineprofiles/library/consumer/EmptyClass.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.benchmark.integration.baselineprofiles.library.consumer
+
+import android.util.Log
+
+object EmptyClass {
+
+ fun doSomething() {
+ Log.d("EmptyClass", "Done.")
+ }
+}
diff --git a/benchmark/integration-tests/baselineprofiles-library-producer/build.gradle b/benchmark/integration-tests/baselineprofiles-library-producer/build.gradle
new file mode 100644
index 0000000..c797473
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-producer/build.gradle
@@ -0,0 +1,60 @@
+import com.android.build.api.dsl.ManagedVirtualDevice
+
+/*
+ * 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.
+ */
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.test")
+ id("kotlin-android")
+ id("androidx.baselineprofiles.producer")
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 23
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ testOptions.managedDevices.devices {
+ pixel6Api31(ManagedVirtualDevice) {
+ device = "Pixel 6"
+ apiLevel = 31
+ systemImageSource = "aosp"
+ }
+ }
+ targetProjectPath = ":benchmark:integration-tests:baselineprofiles-library-build-provider"
+ namespace "androidx.benchmark.integration.baselineprofiles.library.producer"
+}
+
+dependencies {
+ implementation(project(":benchmark:benchmark-junit4"))
+ implementation(project(":benchmark:benchmark-macro-junit4"))
+ implementation(libs.testRules)
+ implementation(libs.testExtJunit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
+ implementation(libs.testUiautomator)
+ implementation(libs.testExtTruth)
+}
+
+baselineProfilesProfileProducer {
+ managedDevices += "pixel6Api31"
+ useConnectedDevices = false
+}
+
+androidx {
+ disableDeviceTests = true
+}
diff --git a/benchmark/integration-tests/baselineprofiles-library-producer/src/main/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-library-producer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..bae036b
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-producer/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<manifest />
diff --git a/benchmark/integration-tests/baselineprofiles-library-producer/src/main/java/androidx/benchmark/integration/baselineprofiles/library/producer/BaselineProfileTest.kt b/benchmark/integration-tests/baselineprofiles-library-producer/src/main/java/androidx/benchmark/integration/baselineprofiles/library/producer/BaselineProfileTest.kt
new file mode 100644
index 0000000..1d5cdec
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-library-producer/src/main/java/androidx/benchmark/integration/baselineprofiles/library/producer/BaselineProfileTest.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.benchmark.integration.baselineprofiles.library.producer
+
+import android.content.Intent
+import android.os.Build
+import androidx.benchmark.DeviceInfo
+import androidx.benchmark.macro.junit4.BaselineProfileRule
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 29)
+class BaselineProfileTest {
+
+ @get:Rule
+ val baselineRule = BaselineProfileRule()
+
+ @Test
+ fun startupBaselineProfile() {
+ assumeTrue(DeviceInfo.isRooted || Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
+
+ // Collects the baseline profile
+ baselineRule.collectBaselineProfile(
+ packageName = PACKAGE_NAME,
+ profileBlock = {
+ startActivityAndWait(Intent(ACTION))
+ device.waitForIdle()
+ }
+ )
+ }
+
+ companion object {
+ private const val PACKAGE_NAME =
+ "androidx.benchmark.integration.baselineprofiles.library.buildprovider"
+ private const val ACTION =
+ "androidx.benchmark.integration.baselineprofiles.EMPTY_ACTIVITY"
+ }
+}
diff --git a/benchmark/integration-tests/baselineprofiles-producer/build.gradle b/benchmark/integration-tests/baselineprofiles-producer/build.gradle
new file mode 100644
index 0000000..c49546c
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-producer/build.gradle
@@ -0,0 +1,60 @@
+import com.android.build.api.dsl.ManagedVirtualDevice
+
+/*
+ * 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.
+ */
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.test")
+ id("kotlin-android")
+ id("androidx.baselineprofiles.producer")
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 23
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ testOptions.managedDevices.devices {
+ pixel6Api31(ManagedVirtualDevice) {
+ device = "Pixel 6"
+ apiLevel = 31
+ systemImageSource = "aosp"
+ }
+ }
+ targetProjectPath = ":benchmark:integration-tests:baselineprofiles-consumer"
+ namespace "androidx.benchmark.integration.baselineprofiles.producer"
+}
+
+dependencies {
+ implementation(project(":benchmark:benchmark-junit4"))
+ implementation(project(":benchmark:benchmark-macro-junit4"))
+ implementation(libs.testRules)
+ implementation(libs.testExtJunit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
+ implementation(libs.testUiautomator)
+ implementation(libs.testExtTruth)
+}
+
+baselineProfilesProfileProducer {
+ managedDevices += "pixel6Api31"
+ useConnectedDevices = false
+}
+
+androidx {
+ disableDeviceTests = true
+}
diff --git a/benchmark/integration-tests/baselineprofiles-producer/src/main/AndroidManifest.xml b/benchmark/integration-tests/baselineprofiles-producer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..bae036b
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-producer/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<manifest />
diff --git a/benchmark/integration-tests/baselineprofiles-producer/src/main/java/androidx/benchmark/integration/baselineprofiles/producer/BaselineProfileTest.kt b/benchmark/integration-tests/baselineprofiles-producer/src/main/java/androidx/benchmark/integration/baselineprofiles/producer/BaselineProfileTest.kt
new file mode 100644
index 0000000..3fbcfea
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-producer/src/main/java/androidx/benchmark/integration/baselineprofiles/producer/BaselineProfileTest.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.benchmark.integration.baselineprofiles.producer
+
+import android.content.Intent
+import android.os.Build
+import androidx.benchmark.DeviceInfo
+import androidx.benchmark.macro.junit4.BaselineProfileRule
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 29)
+class BaselineProfileTest {
+
+ @get:Rule
+ val baselineRule = BaselineProfileRule()
+
+ @Test
+ fun startupBaselineProfile() {
+ assumeTrue(DeviceInfo.isRooted || Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
+
+ // Collects the baseline profile
+ baselineRule.collectBaselineProfile(
+ packageName = PACKAGE_NAME,
+ profileBlock = {
+ startActivityAndWait(Intent(ACTION))
+ device.waitForIdle()
+ }
+ )
+ }
+
+ companion object {
+ private const val PACKAGE_NAME =
+ "androidx.benchmark.integration.baselineprofiles.consumer"
+ private const val ACTION =
+ "androidx.benchmark.integration.baselineprofiles.consumer.EMPTY_ACTIVITY"
+ }
+}
diff --git a/benchmark/integration-tests/baselineprofiles-test-utils/utils.gradle b/benchmark/integration-tests/baselineprofiles-test-utils/utils.gradle
new file mode 100644
index 0000000..4a0a8dd
--- /dev/null
+++ b/benchmark/integration-tests/baselineprofiles-test-utils/utils.gradle
@@ -0,0 +1,26 @@
+// To trigger the baseline profile generation using the different modules the test will call
+// the base generation task `generateBaselineProfiles`. The `AssertEqualsAndCleanUpTask` asserts
+// that the final output is the expected one and if there are no failures cleans up the
+// generated baseline-prof.txt.
+@CacheableTask
+abstract class AssertEqualsAndCleanUpTask extends DefaultTask {
+ @InputFile
+ @PathSensitive(PathSensitivity.NONE)
+ abstract RegularFileProperty getExpectedFile()
+
+ @InputFile
+ @PathSensitive(PathSensitivity.NONE)
+ abstract RegularFileProperty getActualFile()
+
+ @TaskAction
+ void exec() {
+ assert getExpectedFile().get().asFile.text == getActualFile().get().asFile.text
+
+ // This deletes the actual file since it's a test artifact
+ getActualFile().get().asFile.delete()
+ }
+}
+tasks.register("testBaselineProfilesGeneration", AssertEqualsAndCleanUpTask).configure {
+ it.expectedFile.set(project.layout.projectDirectory.file("src/main/expected-baseline-prof.txt"))
+ it.actualFile.set(tasks.named("generateBaselineProfiles").flatMap { it.baselineProfileFile })
+}
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilderTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilderTest.kt
index ccc54b9..d1cd75b7 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilderTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilderTest.kt
@@ -126,6 +126,15 @@
}
@Test
+ fun testValidTestConfigXml_disableDeviceTests() {
+ builder.disableDeviceTests(true)
+ MatcherAssert.assertThat(
+ builder.build(),
+ CoreMatchers.`is`(disableDeviceTestsConfig)
+ )
+ }
+
+ @Test
fun testValidMediaConfigXml_default() {
validate(mediaBuilder.build())
}
@@ -149,6 +158,21 @@
}
}
+private val disableDeviceTestsConfig = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- Copyright (C) 2020 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License")
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions
+ and limitations under the License.-->
+
+""".trimIndent()
+
private val goldenDefaultConfig = """
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 The Android Open Source Project
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
index bf6e0b1..2303c2a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
@@ -16,14 +16,15 @@
package androidx.build
+import androidx.build.buildInfo.CreateLibraryBuildInfoFileTask.Companion.getFrameworksSupportCommitShaAtHead
import androidx.build.checkapi.shouldConfigureApiTasks
import androidx.build.transform.configureAarAsJarForConfiguration
import groovy.lang.Closure
+import java.io.File
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
-import java.io.File
/**
* Extension for [AndroidXImplPlugin] that's responsible for holding configuration options.
@@ -31,6 +32,7 @@
open class AndroidXExtension(val project: Project) {
@JvmField
val LibraryVersions: Map<String, Version>
+
@JvmField
val AllLibraryGroups: List<LibraryGroup>
@@ -84,7 +86,9 @@
}
var name: Property<String?> = project.objects.property(String::class.java)
- fun setName(newName: String) { name.set(newName) }
+ fun setName(newName: String) {
+ name.set(newName)
+ }
/**
* Maven version of the library.
@@ -173,7 +177,7 @@
val groupIdText = if (projectPath.startsWith(":external")) {
projectPath.replace(":external:", "")
} else {
- "androidx.${parentPath.substring(1).replace(':', '.')}"
+ "androidx.${parentPath.substring(1).replace(':', '.')}"
}
// get the library group having that text
@@ -255,8 +259,10 @@
fun isVersionSet(): Boolean {
return versionIsSet
}
+
var description: String? = null
var inceptionYear: String? = null
+
/**
* targetsJavaConsumers = true, if project is intended to be accessed from Java-language
* source code.
@@ -309,7 +315,7 @@
}
internal fun isPublishConfigured(): Boolean = (
- publish != Publish.UNSET ||
+ publish != Publish.UNSET ||
type.publish != Publish.UNSET
)
@@ -332,6 +338,8 @@
var metalavaK2UastEnabled = false
+ var disableDeviceTests = false
+
fun shouldEnforceKotlinStrictApiMode(): Boolean {
return !legacyDisableKotlinStrictApiMode &&
shouldConfigureApiTasks()
@@ -351,6 +359,12 @@
configureAarAsJarForConfiguration(project, name)
}
+ fun getReferenceSha(): Provider<String> {
+ return project.providers.provider {
+ project.getFrameworksSupportCommitShaAtHead()
+ }
+ }
+
companion object {
const val DEFAULT_UNSPECIFIED_VERSION = "unspecified"
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt
index 9ad420c..70232ec 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt
@@ -20,6 +20,7 @@
var appApkName: String? = null
lateinit var applicationId: String
var isBenchmark: Boolean = false
+ var disableDeviceTests: Boolean = false
var isPostsubmit: Boolean = true
lateinit var minSdk: String
var runAllTests: Boolean = true
@@ -31,6 +32,8 @@
fun appApkName(appApkName: String) = apply { this.appApkName = appApkName }
fun applicationId(applicationId: String) = apply { this.applicationId = applicationId }
fun isBenchmark(isBenchmark: Boolean) = apply { this.isBenchmark = isBenchmark }
+ fun disableDeviceTests(disableDeviceTests: Boolean) =
+ apply { this.disableDeviceTests = disableDeviceTests }
fun isPostsubmit(isPostsubmit: Boolean) = apply { this.isPostsubmit = isPostsubmit }
fun minSdk(minSdk: String) = apply { this.minSdk = minSdk }
fun runAllTests(runAllTests: Boolean) = apply { this.runAllTests = runAllTests }
@@ -42,7 +45,11 @@
fun build(): String {
val sb = StringBuilder()
sb.append(XML_HEADER_AND_LICENSE)
- .append(CONFIGURATION_OPEN)
+ if (disableDeviceTests) {
+ return sb.toString()
+ }
+
+ sb.append(CONFIGURATION_OPEN)
.append(MIN_API_LEVEL_CONTROLLER_OBJECT.replace("MIN_SDK", minSdk))
tags.forEach { tag ->
sb.append(TEST_SUITE_TAG_OPTION.replace("TEST_SUITE_TAG", tag))
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
index 0d58d11..5247066 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
@@ -72,6 +72,9 @@
abstract val hasBenchmarkPlugin: Property<Boolean>
@get:Input
+ abstract val disableDeviceTests: Property<Boolean>
+
+ @get:Input
@get:Optional
abstract val benchmarkRunAlsoInterpreted: Property<Boolean>
@@ -176,41 +179,44 @@
}
}
// This section adds metadata tags that will help filter runners to specific modules.
- if (hasBenchmarkPlugin.get()) {
- configBuilder.isBenchmark(true)
- if (configBuilder.isPostsubmit) {
- if (benchmarkRunAlsoInterpreted.get()) {
- configBuilder.tag("microbenchmarks_interpreted")
- }
- configBuilder.tag("microbenchmarks")
- } else {
- // in presubmit, we treat micro benchmarks as regular correctness tests as
- // they run with dryRunMode to check crashes don't happen, without measurement
- configBuilder.tag("androidx_unit_tests")
- }
- } else if (testProjectPath.get().endsWith("macrobenchmark")) {
- // macro benchmarks do not have a dryRunMode, so we don't run them in presubmit
- configBuilder.tag("macrobenchmarks")
+ if (disableDeviceTests.get()) {
+ configBuilder.disableDeviceTests(true)
} else {
- configBuilder.tag("androidx_unit_tests")
- if (testProjectPath.get().startsWith(":compose:")) {
- configBuilder.tag("compose")
- } else if (testProjectPath.get().startsWith(":wear:")) {
- configBuilder.tag("wear")
+ if (hasBenchmarkPlugin.get()) {
+ configBuilder.isBenchmark(true)
+ if (configBuilder.isPostsubmit) {
+ if (benchmarkRunAlsoInterpreted.get()) {
+ configBuilder.tag("microbenchmarks_interpreted")
+ }
+ configBuilder.tag("microbenchmarks")
+ } else {
+ // in presubmit, we treat micro benchmarks as regular correctness tests as
+ // they run with dryRunMode to check crashes don't happen, without measurement
+ configBuilder.tag("androidx_unit_tests")
+ }
+ } else if (testProjectPath.get().endsWith("macrobenchmark")) {
+ // macro benchmarks do not have a dryRunMode, so we don't run them in presubmit
+ configBuilder.tag("macrobenchmarks")
+ } else {
+ configBuilder.tag("androidx_unit_tests")
+ if (testProjectPath.get().startsWith(":compose:")) {
+ configBuilder.tag("compose")
+ } else if (testProjectPath.get().startsWith(":wear:")) {
+ configBuilder.tag("wear")
+ }
}
+ val testApk = testLoader.get().load(testFolder.get())
+ ?: throw RuntimeException("Cannot load required APK for task: $name")
+ val testApkBuiltArtifact = testApk.elements.single()
+ val testName = testApkBuiltArtifact.outputFile
+ .substringAfterLast("/")
+ .renameApkForTesting(testProjectPath.get(), hasBenchmarkPlugin.get())
+ configBuilder.testApkName(testName)
+ .applicationId(testApk.applicationId)
+ .minSdk(minSdk.get().toString())
+ .testRunner(testRunner.get())
+ testApkSha256Report.addFile(testName, testApkBuiltArtifact)
}
- val testApk = testLoader.get().load(testFolder.get())
- ?: throw RuntimeException("Cannot load required APK for task: $name")
- val testApkBuiltArtifact = testApk.elements.single()
- val testName = testApkBuiltArtifact.outputFile
- .substringAfterLast("/")
- .renameApkForTesting(testProjectPath.get(), hasBenchmarkPlugin.get())
- configBuilder.testApkName(testName)
- .applicationId(testApk.applicationId)
- .minSdk(minSdk.get().toString())
- .testRunner(testRunner.get())
- testApkSha256Report.addFile(testName, testApkBuiltArtifact)
-
val resolvedOutputFile: File = outputFile.asFile.get()
if (!resolvedOutputFile.exists()) {
if (!resolvedOutputFile.createNewFile()) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index 5f200c6..040bfcc 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -76,6 +76,8 @@
"${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}$variantName",
GenerateTestConfigurationTask::class.java
) { task ->
+ val androidXExtension = extensions.getByType<AndroidXExtension>()
+
task.testFolder.set(artifacts.get(SingleArtifact.APK))
task.testLoader.set(artifacts.getBuiltArtifactsLoader())
task.outputXml.fileValue(File(getTestConfigDirectory(), xmlName))
@@ -91,12 +93,11 @@
} else {
task.minSdk.set(minSdk)
}
+ task.disableDeviceTests.set(androidXExtension.disableDeviceTests)
val hasBenchmarkPlugin = hasBenchmarkPlugin()
task.hasBenchmarkPlugin.set(hasBenchmarkPlugin)
if (hasBenchmarkPlugin) {
- task.benchmarkRunAlsoInterpreted.set(
- extensions.getByType<AndroidXExtension>().benchmarkRunAlsoInterpreted
- )
+ task.benchmarkRunAlsoInterpreted.set(androidXExtension.benchmarkRunAlsoInterpreted)
}
task.testRunner.set(testRunner)
task.testProjectPath.set(path)
@@ -333,6 +334,8 @@
val configTask = getOrCreateMacrobenchmarkConfigTask(variantName)
if (path.endsWith("macrobenchmark")) {
configTask.configure { task ->
+ val androidXExtension = extensions.getByType<AndroidXExtension>()
+
task.testFolder.set(artifacts.get(SingleArtifact.APK))
task.testLoader.set(artifacts.getBuiltArtifactsLoader())
task.outputXml.fileValue(
@@ -360,6 +363,7 @@
)
)
task.minSdk.set(minSdk)
+ task.disableDeviceTests.set(androidXExtension.disableDeviceTests)
task.hasBenchmarkPlugin.set(this.hasBenchmarkPlugin())
task.testRunner.set(testRunner)
task.testProjectPath.set(this.path)
diff --git a/buildSrc/public/build.gradle b/buildSrc/public/build.gradle
index e37e156..9ccda13 100644
--- a/buildSrc/public/build.gradle
+++ b/buildSrc/public/build.gradle
@@ -1,19 +1,31 @@
apply from: "../shared.gradle"
sourceSets {
+
+ // Benchmark
main.java.srcDirs += "${supportRootFolder}/benchmark/gradle-plugin/src/main/kotlin"
main.resources.srcDirs += "${supportRootFolder}/benchmark/gradle-plugin/src/main/resources"
+ // Benchmark darwin
main.java.srcDirs += "${supportRootFolder}/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin"
main.resources.srcDirs += "${supportRootFolder}/benchmark/benchmark-darwin-gradle-plugin/src/main/resources"
+ // Baseline profile
+ main.java.srcDirs += "${supportRootFolder}" +
+ "/benchmark/baseline-profiles-gradle-plugin/src/main/kotlin"
+ main.resources.srcDirs += "${supportRootFolder}" +
+ "/benchmark/baseline-profiles-gradle-plugin/src/main/resources"
+
+ // Inspection
main.java.srcDirs += "${supportRootFolder}/inspection/inspection-gradle-plugin/src/main/kotlin"
main.resources.srcDirs += "${supportRootFolder}/inspection/inspection-gradle-plugin/src/main" +
"/resources"
+ // Compose
main.java.srcDirs += "${supportRootFolder}/compose/material/material/icons/generator/src/main" +
"/kotlin"
+ // Glance
main.java.srcDirs += "${supportRootFolder}/glance/glance-appwidget/glance-layout-generator/" +
"src/main/kotlin"
}
@@ -29,6 +41,18 @@
id = "androidx.benchmark"
implementationClass = "androidx.benchmark.gradle.BenchmarkPlugin"
}
+ baselineProfilesProducer {
+ id = "androidx.baselineprofiles.producer"
+ implementationClass = "androidx.baselineprofiles.gradle.producer.BaselineProfilesProducerPlugin"
+ }
+ baselineProfilesConsumer {
+ id = "androidx.baselineprofiles.consumer"
+ implementationClass = "androidx.baselineprofiles.gradle.consumer.BaselineProfilesConsumerPlugin"
+ }
+ baselineProfilesBuildProvider {
+ id = "androidx.baselineprofiles.buildprovider"
+ implementationClass = "androidx.baselineprofiles.gradle.buildprovider.BaselineProfilesBuildProviderPlugin"
+ }
inspection {
id = "androidx.inspection"
implementationClass = "androidx.inspection.gradle.InspectionPlugin"
diff --git a/buildSrc/shared.gradle b/buildSrc/shared.gradle
index 7ac3cae..c96dfa4 100644
--- a/buildSrc/shared.gradle
+++ b/buildSrc/shared.gradle
@@ -46,6 +46,9 @@
implementation(libs.kotlinPoet) // needed to compile material-icon-generator
implementation(libs.xmlpull) // needed to compile material-icon-generator
+ implementation(libs.protobuf) // needed to compile baseline-profile gradle plugins
+ implementation(libs.agpTestingPlatformCoreProto) // needed to compile baseline-profile gradle plugins
+
// dependencies that aren't used by buildSrc directly but that we resolve here so that the
// root project doesn't need to re-resolve them and their dependencies on every build
runtimeOnly(libs.hiltAndroidGradlePluginz)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
index a9606f1..4c03b51 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
@@ -27,6 +27,7 @@
import androidx.camera.camera2.pipe.integration.config.CameraAppComponent
import androidx.camera.core.impl.AttachedSurfaceInfo
import androidx.camera.core.impl.CameraDeviceSurfaceManager
+import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.SurfaceConfig
import androidx.camera.core.impl.UseCaseConfig
@@ -120,25 +121,24 @@
}
/**
- * Retrieves a map of suggested resolutions for the given list of use cases.
+ * Retrieves a map of suggested stream specifications for the given list of use cases.
*
* @param cameraId the camera id of the camera device used by the use cases
* @param existingSurfaces list of surfaces already configured and used by the camera. The
* resolutions for these surface can not change.
* @param newUseCaseConfigs list of configurations of the use cases that will be given a
- * suggested resolution
- * @return map of suggested resolutions for given use cases
+ * suggested stream specification
+ * @return map of suggested stream specifications for given use cases
* @throws IllegalArgumentException if {@code newUseCaseConfigs} is an empty list, if
* there isn't a supported combination of surfaces
* available, or if the {@code cameraId}
* is not a valid id.
*/
- override fun getSuggestedResolutions(
+ override fun getSuggestedStreamSpecs(
cameraId: String,
existingSurfaces: List<AttachedSurfaceInfo>,
newUseCaseConfigs: List<UseCaseConfig<*>>
- ): Map<UseCaseConfig<*>, Size> {
- checkIfSupportedCombinationExist(cameraId)
+ ): Map<UseCaseConfig<*>, StreamSpec> {
if (!checkIfSupportedCombinationExist(cameraId)) {
throw IllegalArgumentException(
@@ -146,7 +146,7 @@
)
}
- return supportedSurfaceCombinationMap[cameraId]!!.getSuggestedResolutions(
+ return supportedSurfaceCombinationMap[cameraId]!!.getSuggestedStreamSpecifications(
existingSurfaces,
newUseCaseConfigs
)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
index 7b1ad0f..05cb202 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
@@ -37,6 +37,7 @@
import androidx.camera.core.impl.CamcorderProfileProxy
import androidx.camera.core.impl.ImageFormatConstants
import androidx.camera.core.impl.ImageOutputConfig
+import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.SurfaceCombination
import androidx.camera.core.impl.SurfaceConfig
import androidx.camera.core.impl.SurfaceSizeDefinition
@@ -120,20 +121,19 @@
}
/**
- * Finds the suggested resolutions of the newly added UseCaseConfig.
+ * Finds the suggested stream specification of the newly added UseCaseConfig.
*
* @param existingSurfaces the existing surfaces.
* @param newUseCaseConfigs newly added UseCaseConfig.
- * @return the suggested resolutions, which is a mapping from UseCaseConfig to the suggested
- * resolution.
+ * @return the suggested stream specs, which is a mapping from UseCaseConfig to the suggested
+ * stream specification.
* @throws IllegalArgumentException if the suggested solution for newUseCaseConfigs cannot be
- * found. This may be due to no available output size or no
- * available surface combination.
+ * found. This may be due to no available output size or no available surface combination.
*/
- fun getSuggestedResolutions(
+ fun getSuggestedStreamSpecifications(
existingSurfaces: List<AttachedSurfaceInfo>,
newUseCaseConfigs: List<UseCaseConfig<*>>
- ): Map<UseCaseConfig<*>, Size> {
+ ): Map<UseCaseConfig<*>, StreamSpec> {
refreshPreviewSize()
val surfaceConfigs: MutableList<SurfaceConfig> = ArrayList()
for (scc in existingSurfaces) {
@@ -176,7 +176,7 @@
supportedOutputSizesList
)
- var suggestedResolutionsMap: Map<UseCaseConfig<*>, Size>? = null
+ var suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec>? = null
// Transform use cases to SurfaceConfig list and find the first (best) workable combination
for (possibleSizeList in allPossibleSizeArrangements) {
// Attach SurfaceConfig of original use cases since it will impact the new use cases
@@ -200,19 +200,19 @@
// Check whether the SurfaceConfig combination can be supported
if (checkSupported(surfaceConfigList)) {
- suggestedResolutionsMap = HashMap()
+ suggestedStreamSpecMap = HashMap()
for (useCaseConfig in newUseCaseConfigs) {
- suggestedResolutionsMap.put(
+ suggestedStreamSpecMap.put(
useCaseConfig,
- possibleSizeList[useCasesPriorityOrder.indexOf(
+ StreamSpec.builder(possibleSizeList[useCasesPriorityOrder.indexOf(
newUseCaseConfigs.indexOf(useCaseConfig)
- )]
+ )]).build()
)
}
break
}
}
- if (suggestedResolutionsMap == null) {
+ if (suggestedStreamSpecMap == null) {
throw java.lang.IllegalArgumentException(
"No supported surface combination is found for camera device - Id : " +
cameraId + " and Hardware level: " + hardwareLevel +
@@ -221,7 +221,7 @@
" New configs: " + newUseCaseConfigs
)
}
- return suggestedResolutionsMap
+ return suggestedStreamSpecMap
}
// Utility classes and methods:
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
index 754ee01..d49b7a9 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
@@ -39,6 +39,7 @@
import androidx.camera.core.impl.ImmediateSurface
import androidx.camera.core.impl.MutableOptionsBundle
import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.UseCaseConfig
import androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER
import androidx.camera.core.impl.UseCaseConfigFactory
@@ -72,10 +73,10 @@
override fun getUseCaseConfigBuilder(config: Config) =
Builder(cameraProperties, displayInfoManager)
- override fun onSuggestedResolutionUpdated(suggestedResolution: Size): Size {
+ override fun onSuggestedStreamSpecUpdated(suggestedStreamSpec: StreamSpec): StreamSpec {
updateSessionConfig(createPipeline().build())
notifyActive()
- return meteringSurfaceSize
+ return StreamSpec.builder(meteringSurfaceSize).build()
}
override fun onUnbind() {
@@ -87,9 +88,9 @@
/** Sets up the use case's session configuration, mainly its [DeferrableSurface]. */
fun setupSession() {
- // The suggested resolution passed to `updateSuggestedResolution` doesn't matter since
+ // The suggested stream spec passed to `updateSuggestedStreamSpec` doesn't matter since
// this use case uses the min preview size.
- updateSuggestedResolution(DEFAULT_PREVIEW_SIZE)
+ updateSuggestedStreamSpec(StreamSpec.builder(DEFAULT_PREVIEW_SIZE).build())
}
private fun createPipeline(): SessionConfig.Builder {
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
index 98e6e74..8ca5d39 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
@@ -45,6 +45,7 @@
import androidx.camera.core.Preview
import androidx.camera.core.SurfaceOrientedMeteringPointFactory
import androidx.camera.core.UseCase
+import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.testing.SurfaceTextureProvider
import androidx.camera.testing.fakes.FakeCamera
@@ -482,7 +483,7 @@
fun customFovAdjusted() {
// 16:9 to 4:3
val useCase = FakeUseCase()
- useCase.updateSuggestedResolution(Size(1920, 1080))
+ useCase.updateSuggestedStreamSpec(StreamSpec.builder(Size(1920, 1080)).build())
val factory = SurfaceOrientedMeteringPointFactory(1.0f, 1.0f, useCase)
val point = factory.createPoint(0f, 0f)
@@ -1244,7 +1245,7 @@
)
}
- private fun createPreview(suggestedResolution: Size) =
+ private fun createPreview(suggestedStreamSpecResolution: Size) =
Preview.Builder()
.setCaptureOptionUnpacker { _, _ -> }
.setSessionOptionUnpacker() { _, _ -> }
@@ -1255,6 +1256,8 @@
)
}.also {
it.bindToCamera(FakeCamera("0"), null, null)
- it.updateSuggestedResolution(suggestedResolution)
+ it.updateSuggestedStreamSpec(
+ StreamSpec.builder(suggestedStreamSpecResolution).build()
+ )
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
index dc4e3d6..59295e2 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -54,6 +54,7 @@
import androidx.camera.core.impl.CameraThreadConfig
import androidx.camera.core.impl.MutableStateObservable
import androidx.camera.core.impl.Observable
+import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.SurfaceCombination
import androidx.camera.core.impl.SurfaceConfig
import androidx.camera.core.impl.UseCaseConfig
@@ -114,11 +115,16 @@
private val portraitPixelArraySize = Size(3024, 4032)
private val displaySize = Size(720, 1280)
private val vgaSize = Size(640, 480)
+ private val vgaSizeStreamSpec = StreamSpec.builder(vgaSize).build()
private val previewSize = Size(1280, 720)
+ private val previewSizeStreamSpec = StreamSpec.builder(previewSize).build()
private val recordSize = Size(3840, 2160)
+ private val recordSizeStreamSpec = StreamSpec.builder(recordSize).build()
private val maximumSize = Size(4032, 3024)
+ private val maximumSizeStreamSpec = StreamSpec.builder(maximumSize).build()
private val legacyVideoMaximumVideoSize = Size(1920, 1080)
private val mod16Size = Size(960, 544)
+ private val mod16SizeStreamSpec = StreamSpec.builder(mod16Size).build()
private val profileUhd = CamcorderProfileUtil.createCamcorderProfileProxy(
CamcorderProfile.QUALITY_2160P, recordSize.width, recordSize
.height
@@ -470,14 +476,14 @@
useCases,
useCaseConfigFactory
)
- val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+ val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
emptyList(),
ArrayList(useCaseToConfigMap.values)
)
- val selectedSize = suggestedResolutionMap[useCaseToConfigMap[fakeUseCase]]!!
+ val selectedStreamSpec = suggestedStreamSpecMap[useCaseToConfigMap[fakeUseCase]]!!
val resultAspectRatio = Rational(
- selectedSize.width,
- selectedSize.height
+ selectedStreamSpec.resolution.width,
+ selectedStreamSpec.resolution.height
)
assertThat(resultAspectRatio).isEqualTo(aspectRatio169)
}
@@ -566,26 +572,27 @@
useCases,
useCaseConfigFactory
)
- val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+ val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
emptyList(),
ArrayList(useCaseToConfigMap.values)
)
- val previewSize = suggestedResolutionMap[useCaseToConfigMap[preview]]
- val imageCaptureSize = suggestedResolutionMap[useCaseToConfigMap[imageCapture]]
- val imageAnalysisSize = suggestedResolutionMap[useCaseToConfigMap[imageAnalysis]]
- assert(previewSize != null)
+ val previewSize = suggestedStreamSpecMap[useCaseToConfigMap[preview]]!!.resolution
+ val imageCaptureSize = suggestedStreamSpecMap[useCaseToConfigMap[imageCapture]]!!.resolution
+ val imageAnalysisSize =
+ suggestedStreamSpecMap[useCaseToConfigMap[imageAnalysis]]!!.resolution
+
val previewAspectRatio = Rational(
- previewSize!!.width,
+ previewSize.width,
previewSize.height
)
- assert(imageCaptureSize != null)
+
val imageCaptureAspectRatio = Rational(
- imageCaptureSize!!.width,
+ imageCaptureSize.width,
imageCaptureSize.height
)
- assert(imageAnalysisSize != null)
+
val imageAnalysisAspectRatio = Rational(
- imageAnalysisSize!!.width,
+ imageAnalysisSize.width,
imageAnalysisSize.height
)
@@ -625,7 +632,7 @@
useCases,
useCaseConfigFactory
)
- val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+ val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
emptyList(),
ArrayList(useCaseToConfigMap.values)
)
@@ -642,7 +649,7 @@
// Checks the mechanism has filtered out the sizes which are smaller than default size
// 480p.
- val previewSize = suggestedResolutionMap[useCaseToConfigMap[preview]]
+ val previewSize = suggestedStreamSpecMap[useCaseToConfigMap[preview]]
assertThat(previewSize).isNotEqualTo(preconditionSize)
}
@@ -662,12 +669,13 @@
val imageCapture = ImageCapture.Builder().setTargetResolution(
targetResolution
).setTargetRotation(Surface.ROTATION_90).build()
- val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
- emptyList(),
- listOf(imageCapture.currentConfig)
- )
+ val suggestedStreamSpecMap =
+ supportedSurfaceCombination.getSuggestedStreamSpecifications(
+ emptyList(),
+ listOf(imageCapture.currentConfig)
+ )
assertThat(targetResolution).isEqualTo(
- suggestedResolutionMap[imageCapture.currentConfig]
+ suggestedStreamSpecMap[imageCapture.currentConfig]?.resolution
)
}
}
@@ -687,17 +695,17 @@
val imageCapture = ImageCapture.Builder().setTargetResolution(
targetResolution
).setTargetRotation(Surface.ROTATION_90).build()
- val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+ val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
emptyList(),
listOf(imageCapture.currentConfig)
)
assertThat(Size(1280, 720)).isEqualTo(
- suggestedResolutionMap[imageCapture.currentConfig]
+ suggestedStreamSpecMap[imageCapture.currentConfig]?.resolution
)
}
@Test
- fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
+ fun suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice() {
setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
val supportedSurfaceCombination = SupportedSurfaceCombination(
context, fakeCameraMetadata,
@@ -720,7 +728,7 @@
useCaseConfigFactory
)
assertThrows(IllegalArgumentException::class.java) {
- supportedSurfaceCombination.getSuggestedResolutions(
+ supportedSurfaceCombination.getSuggestedStreamSpecifications(
emptyList(),
ArrayList(useCaseToConfigMap.values)
)
@@ -728,7 +736,7 @@
}
@Test
- fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice() {
+ fun suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice() {
setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
val supportedSurfaceCombination = SupportedSurfaceCombination(
context, fakeCameraMetadata,
@@ -753,7 +761,7 @@
useCaseConfigFactory
)
assertThrows(IllegalArgumentException::class.java) {
- supportedSurfaceCombination.getSuggestedResolutions(
+ supportedSurfaceCombination.getSuggestedStreamSpecifications(
emptyList(),
ArrayList(useCaseToConfigMap.values)
)
@@ -762,7 +770,7 @@
// (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
@Test
- fun suggestedResolutionsForMixedUseCaseInLimitedDevice() {
+ fun suggestedStreamSpecsForMixedUseCaseInLimitedDevice() {
setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
val supportedSurfaceCombination = SupportedSurfaceCombination(
context, fakeCameraMetadata,
@@ -784,29 +792,29 @@
useCases,
useCaseConfigFactory
)
- val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
- supportedSurfaceCombination.getSuggestedResolutions(
+ val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
+ supportedSurfaceCombination.getSuggestedStreamSpecifications(
emptyList(),
ArrayList(useCaseToConfigMap.values)
)
// (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
- assertThat(suggestedResolutionMap).containsEntry(
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[imageCapture],
- recordSize
+ recordSizeStreamSpec
)
- assertThat(suggestedResolutionMap).containsEntry(
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[videoCapture],
- recordSize
+ recordSizeStreamSpec
)
- assertThat(suggestedResolutionMap).containsEntry(
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[preview],
- previewSize
+ previewSizeStreamSpec
)
}
@Test
- fun suggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage() {
+ fun suggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage() {
setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
val supportedSurfaceCombination = SupportedSurfaceCombination(
context, fakeCameraMetadata,
@@ -833,8 +841,8 @@
useCases,
useCaseConfigFactory
)
- val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
- supportedSurfaceCombination.getSuggestedResolutions(
+ val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
+ supportedSurfaceCombination.getSuggestedStreamSpecifications(
emptyList(),
ArrayList(useCaseToConfigMap.values)
)
@@ -842,17 +850,17 @@
// There are two possible combinations in Full level device
// (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD) => should be applied
// (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM)
- assertThat(suggestedResolutionMap).containsEntry(
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[imageCapture],
- recordSize
+ recordSizeStreamSpec
)
- assertThat(suggestedResolutionMap).containsEntry(
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[videoCapture],
- recordSize
+ recordSizeStreamSpec
)
- assertThat(suggestedResolutionMap).containsEntry(
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[preview],
- previewSize
+ previewSizeStreamSpec
)
}
@@ -883,8 +891,8 @@
useCases,
useCaseConfigFactory
)
- val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
- supportedSurfaceCombination.getSuggestedResolutions(
+ val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
+ supportedSurfaceCombination.getSuggestedStreamSpecifications(
emptyList(),
ArrayList(useCaseToConfigMap.values)
)
@@ -892,22 +900,22 @@
// There are two possible combinations in Full level device
// (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
// (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM) => should be applied
- assertThat(suggestedResolutionMap).containsEntry(
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[imageCapture],
- maximumSize
+ maximumSizeStreamSpec
)
- assertThat(suggestedResolutionMap).containsEntry(
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[videoCapture],
- previewSize
+ previewSizeStreamSpec
) // Quality.HD
- assertThat(suggestedResolutionMap).containsEntry(
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[preview],
- previewSize
+ previewSizeStreamSpec
)
}
@Test
- fun suggestedResolutionsWithSameSupportedListForDifferentUseCases() {
+ fun suggestedStreamSpecsWithSameSupportedListForDifferentUseCases() {
setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
val supportedSurfaceCombination = SupportedSurfaceCombination(
context, fakeCameraMetadata,
@@ -938,22 +946,22 @@
useCases,
useCaseConfigFactory
)
- val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
- supportedSurfaceCombination.getSuggestedResolutions(
+ val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
+ supportedSurfaceCombination.getSuggestedStreamSpecifications(
emptyList(),
ArrayList(useCaseToConfigMap.values)
)
- assertThat(suggestedResolutionMap).containsEntry(
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[imageCapture],
- previewSize
+ previewSizeStreamSpec
)
- assertThat(suggestedResolutionMap).containsEntry(
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[preview],
- previewSize
+ previewSizeStreamSpec
)
- assertThat(suggestedResolutionMap).containsEntry(
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[imageAnalysis],
- previewSize
+ previewSizeStreamSpec
)
}
@@ -993,9 +1001,9 @@
}
@Test
- fun suggestedResolutionsForCustomizedSupportedResolutions() {
+ fun suggestedStreamSpecsForCustomizedSupportedResolutions() {
- // Checks all suggested resolutions will become 640x480.
+ // Checks all suggested stream specs will have their resolutions become 640x480.
setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
val supportedSurfaceCombination = SupportedSurfaceCombination(
context, fakeCameraMetadata,
@@ -1025,24 +1033,24 @@
useCases,
useCaseConfigFactory
)
- val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
- supportedSurfaceCombination.getSuggestedResolutions(
+ val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
+ supportedSurfaceCombination.getSuggestedStreamSpecifications(
emptyList(),
ArrayList(useCaseToConfigMap.values)
)
- // Checks all suggested resolutions will become 640x480.
- assertThat(suggestedResolutionMap).containsEntry(
+ // Checks all suggested stream specs will have their resolutions become 640x480.
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[imageCapture],
- vgaSize
+ vgaSizeStreamSpec
)
- assertThat(suggestedResolutionMap).containsEntry(
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[videoCapture],
- vgaSize
+ vgaSizeStreamSpec
)
- assertThat(suggestedResolutionMap).containsEntry(
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[preview],
- vgaSize
+ vgaSizeStreamSpec
)
}
@@ -1204,18 +1212,18 @@
useCases,
useCaseConfigFactory
)
- val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
- supportedSurfaceCombination.getSuggestedResolutions(
+ val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
+ supportedSurfaceCombination.getSuggestedStreamSpecifications(
emptyList(),
ArrayList(useCaseToConfigMap.values)
)
- assertThat(suggestedResolutionMap).containsEntry(
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[preview],
- mod16Size
+ mod16SizeStreamSpec
)
- assertThat(suggestedResolutionMap).containsEntry(
+ assertThat(suggestedStreamSpecMap).containsEntry(
useCaseToConfigMap[imageCapture],
- mod16Size
+ mod16SizeStreamSpec
)
}
@@ -2201,13 +2209,13 @@
val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
vgaSize
).setTargetRotation(Surface.ROTATION_90).build()
- val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+ val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
emptyList(),
listOf(useCase.currentConfig)
)
// Checks 640x480 is final selected for the use case.
- assertThat(suggestedResolutionMap[useCase.currentConfig]).isEqualTo(vgaSize)
+ assertThat(suggestedStreamSpecMap[useCase.currentConfig]?.resolution).isEqualTo(vgaSize)
}
@Test
@@ -2226,13 +2234,13 @@
// Sets the max resolution as 720x1280
val useCase = FakeUseCaseConfig.Builder().setMaxResolution(displaySize).build()
- val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+ val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
emptyList(),
listOf(useCase.currentConfig)
)
// Checks 480x480 is final selected for the use case.
- assertThat(suggestedResolutionMap[useCase.currentConfig]).isEqualTo(
+ assertThat(suggestedStreamSpecMap[useCase.currentConfig]?.resolution).isEqualTo(
Size(480, 480)
)
}
@@ -2277,11 +2285,11 @@
useCases,
useCaseConfigFactory
)
- val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+ val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
emptyList(),
ArrayList(useCaseToConfigMap.values)
)
- assertThat(suggestedResolutionMap[useCaseToConfigMap[imageAnalysis]]).isEqualTo(
+ assertThat(suggestedStreamSpecMap[useCaseToConfigMap[imageAnalysis]]?.resolution).isEqualTo(
previewSize
)
}
@@ -2329,11 +2337,11 @@
useCases,
useCaseConfigFactory
)
- val suggestedResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
+ val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
emptyList(),
ArrayList(useCaseToConfigMap.values)
)
- assertThat(suggestedResolutionMap[useCaseToConfigMap[imageAnalysis]]).isEqualTo(
+ assertThat(suggestedStreamSpecMap[useCaseToConfigMap[imageAnalysis]]?.resolution).isEqualTo(
recordSize
)
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
index cc035a9..8687489 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
@@ -24,6 +24,7 @@
import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.core.impl.StreamSpec
import androidx.test.core.app.ApplicationProvider
import org.junit.After
import org.junit.Assert.assertEquals
@@ -41,7 +42,7 @@
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class MeteringRepeatingTest {
companion object {
- val dummyZeroSize = Size(0, 0)
+ val dummyZeroSizeStreamSpec = StreamSpec.builder(Size(0, 0)).build()
val dummySizeListWithout640x480 = listOf(
Size(4160, 3120),
@@ -130,7 +131,7 @@
fun attachedSurfaceResolutionIsLargestLessThan640x480_when640x480NotPresentInOutputSizes() {
meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListWithout640x480)
- meteringRepeating.updateSuggestedResolution(dummyZeroSize)
+ meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec)
assertEquals(Size(320, 240), meteringRepeating.attachedSurfaceResolution)
}
@@ -139,7 +140,7 @@
fun attachedSurfaceResolutionIs640x480_when640x480PresentInOutputSizes() {
meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListWith640x480)
- meteringRepeating.updateSuggestedResolution(dummyZeroSize)
+ meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec)
assertEquals(Size(640, 480), meteringRepeating.attachedSurfaceResolution)
}
@@ -148,7 +149,7 @@
fun attachedSurfaceResolutionFallsBackToMinimum_whenAllOutputSizesLargerThan640x480() {
meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListWithoutSmaller)
- meteringRepeating.updateSuggestedResolution(dummyZeroSize)
+ meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec)
assertEquals(Size(1280, 720), meteringRepeating.attachedSurfaceResolution)
}
@@ -157,7 +158,7 @@
fun attachedSurfaceResolutionIsLargestWithinPreviewSize_whenAllOutputSizesLessThan640x480() {
meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListSmallerThan640x480)
- meteringRepeating.updateSuggestedResolution(dummyZeroSize)
+ meteringRepeating.updateSuggestedStreamSpec(dummyZeroSizeStreamSpec)
assertEquals(Size(320, 480), meteringRepeating.attachedSurfaceResolution)
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
index 8641662..1617485 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
@@ -30,6 +30,7 @@
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
import androidx.camera.core.UseCase
+import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.testing.SurfaceTextureProvider
import androidx.camera.testing.fakes.FakeCamera
@@ -269,6 +270,6 @@
private fun UseCase.simulateActivation() {
bindToCamera(FakeCamera("0"), null, null)
- updateSuggestedResolution(Size(640, 480))
+ updateSuggestedStreamSpec(StreamSpec.builder(Size(640, 480)).build())
}
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
index edf21f9..40b5167 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
@@ -35,6 +35,7 @@
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
+import androidx.camera.camera2.pipe.CameraMetadata
import java.util.concurrent.Executor
@RequiresApi(Build.VERSION_CODES.M)
@@ -322,6 +323,12 @@
@JvmStatic
@DoNotInline
+ fun getAvailableStreamUseCases(cameraMetadata: CameraMetadata): LongArray? {
+ return cameraMetadata[CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES]
+ }
+
+ @JvmStatic
+ @DoNotInline
fun getStreamUseCase(outputConfig: OutputConfiguration): Long {
return outputConfig.streamUseCase
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
index 56c25a7..f9767ba 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
@@ -24,6 +24,7 @@
import android.view.Surface
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.compat.OutputConfigurationWrapper.Companion.SURFACE_GROUP_ID_NONE
import androidx.camera.camera2.pipe.config.Camera2ControllerScope
@@ -180,14 +181,21 @@
constructor(
private val threads: Threads,
private val streamGraph: StreamGraphImpl,
- private val graphConfig: CameraGraph.Config
+ private val graphConfig: CameraGraph.Config,
+ private val camera2MetadataProvider: Camera2MetadataProvider
) : CaptureSessionFactory {
override fun create(
cameraDevice: CameraDeviceWrapper,
surfaces: Map<StreamId, Surface>,
captureSessionState: CaptureSessionState
): Map<StreamId, OutputConfigurationWrapper> {
- val outputs = buildOutputConfigurations(graphConfig, streamGraph, surfaces)
+ val outputs = buildOutputConfigurations(
+ graphConfig,
+ streamGraph,
+ surfaces,
+ camera2MetadataProvider,
+ cameraDevice.cameraId
+ )
if (outputs.all.isEmpty()) {
Log.warn { "Failed to create OutputConfigurations for $graphConfig" }
return emptyMap()
@@ -224,7 +232,8 @@
constructor(
private val threads: Threads,
private val graphConfig: CameraGraph.Config,
- private val streamGraph: StreamGraphImpl
+ private val streamGraph: StreamGraphImpl,
+ private val camera2MetadataProvider: Camera2MetadataProvider
) : CaptureSessionFactory {
override fun create(
cameraDevice: CameraDeviceWrapper,
@@ -238,7 +247,13 @@
CameraGraph.OperatingMode.HIGH_SPEED -> SessionConfigData.SESSION_TYPE_HIGH_SPEED
}
- val outputs = buildOutputConfigurations(graphConfig, streamGraph, surfaces)
+ val outputs = buildOutputConfigurations(
+ graphConfig,
+ streamGraph,
+ surfaces,
+ camera2MetadataProvider,
+ cameraDevice.cameraId
+ )
if (outputs.all.isEmpty()) {
Log.warn { "Failed to create OutputConfigurations for $graphConfig" }
return emptyMap()
@@ -277,7 +292,9 @@
internal fun buildOutputConfigurations(
graphConfig: CameraGraph.Config,
streamGraph: StreamGraphImpl,
- surfaces: Map<StreamId, Surface>
+ surfaces: Map<StreamId, Surface>,
+ camera2MetadataProvider: Camera2MetadataProvider,
+ cameraId: CameraId
): OutputConfigurations {
val allOutputs = arrayListOf<OutputConfigurationWrapper>()
val deferredOutputs = mutableMapOf<StreamId, OutputConfigurationWrapper>()
@@ -303,23 +320,24 @@
}
if (outputConfig.deferrable && outputSurfaces.size != outputConfig.streams.size) {
- val output =
- AndroidOutputConfiguration.create(
- null,
- size = outputConfig.size,
- outputType = outputConfig.deferredOutputType!!,
- mirrorMode = outputConfig.mirrorMode,
- timestampBase = outputConfig.timestampBase,
- dynamicRangeProfile = outputConfig.dynamicRangeProfile,
- streamUseCase = outputConfig.streamUseCase,
- surfaceSharing = outputConfig.surfaceSharing,
- surfaceGroupId = outputConfig.groupNumber ?: SURFACE_GROUP_ID_NONE,
- physicalCameraId =
- if (outputConfig.camera != graphConfig.camera) {
- outputConfig.camera
- } else {
- null
- })
+ val output = AndroidOutputConfiguration.create(
+ null,
+ size = outputConfig.size,
+ outputType = outputConfig.deferredOutputType!!,
+ mirrorMode = outputConfig.mirrorMode,
+ timestampBase = outputConfig.timestampBase,
+ dynamicRangeProfile = outputConfig.dynamicRangeProfile,
+ streamUseCase = outputConfig.streamUseCase,
+ surfaceSharing = outputConfig.surfaceSharing,
+ surfaceGroupId = outputConfig.groupNumber ?: SURFACE_GROUP_ID_NONE,
+ physicalCameraId = if (outputConfig.camera != graphConfig.camera) {
+ outputConfig.camera
+ } else {
+ null
+ },
+ cameraId = cameraId,
+ camera2MetadataProvider = camera2MetadataProvider
+ )
if (output == null) {
Log.warn { "Failed to create AndroidOutputConfiguration for $outputConfig" }
continue
@@ -337,22 +355,23 @@
"Surfaces are not yet available for $outputConfig!" +
" Missing surfaces for $missingStreams!"
}
- val output =
- AndroidOutputConfiguration.create(
- outputSurfaces.first(),
- mirrorMode = outputConfig.mirrorMode,
- timestampBase = outputConfig.timestampBase,
- dynamicRangeProfile = outputConfig.dynamicRangeProfile,
- streamUseCase = outputConfig.streamUseCase,
- size = outputConfig.size,
- surfaceSharing = outputConfig.surfaceSharing,
- surfaceGroupId = outputConfig.groupNumber ?: SURFACE_GROUP_ID_NONE,
- physicalCameraId =
- if (outputConfig.camera != graphConfig.camera) {
- outputConfig.camera
- } else {
- null
- })
+ val output = AndroidOutputConfiguration.create(
+ outputSurfaces.first(),
+ mirrorMode = outputConfig.mirrorMode,
+ timestampBase = outputConfig.timestampBase,
+ dynamicRangeProfile = outputConfig.dynamicRangeProfile,
+ streamUseCase = outputConfig.streamUseCase,
+ size = outputConfig.size,
+ surfaceSharing = outputConfig.surfaceSharing,
+ surfaceGroupId = outputConfig.groupNumber ?: SURFACE_GROUP_ID_NONE,
+ physicalCameraId = if (outputConfig.camera != graphConfig.camera) {
+ outputConfig.camera
+ } else {
+ null
+ },
+ cameraId = cameraId,
+ camera2MetadataProvider = camera2MetadataProvider
+ )
if (output == null) {
Log.warn { "Failed to create AndroidOutputConfiguration for $outputConfig" }
continue
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
index 550563a..d5b60d1 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
@@ -142,7 +142,9 @@
size: Size? = null,
surfaceSharing: Boolean = false,
surfaceGroupId: Int = SURFACE_GROUP_ID_NONE,
- physicalCameraId: CameraId? = null
+ physicalCameraId: CameraId? = null,
+ cameraId: CameraId? = null,
+ camera2MetadataProvider: Camera2MetadataProvider? = null,
): OutputConfigurationWrapper? {
check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
@@ -172,7 +174,8 @@
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
throw IllegalStateException(
"Deferred OutputConfigurations are not supported on API " +
- "${Build.VERSION.SDK_INT} (requires API ${Build.VERSION_CODES.O})")
+ "${Build.VERSION.SDK_INT} (requires API ${Build.VERSION_CODES.O})"
+ )
}
check(size != null) {
@@ -241,9 +244,14 @@
}
}
- if (streamUseCase != null) {
+ if (streamUseCase != null && cameraId != null && camera2MetadataProvider != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- Api33Compat.setStreamUseCase(configuration, streamUseCase.value)
+ val cameraMetadata = camera2MetadataProvider.awaitCameraMetadata(cameraId)
+ val availableStreamUseCases =
+ Api33Compat.getAvailableStreamUseCases(cameraMetadata)
+ if (availableStreamUseCases?.contains(streamUseCase.value) == true) {
+ Api33Compat.setStreamUseCase(configuration, streamUseCase.value)
+ }
}
}
@@ -256,7 +264,8 @@
} else {
1
},
- physicalCameraId)
+ physicalCameraId
+ )
}
private fun OutputConfiguration.enableSurfaceSharingCompat() {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactoryTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactoryTest.kt
index c418fe9..c2bcc17 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactoryTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactoryTest.kt
@@ -23,6 +23,8 @@
import android.util.Size
import android.view.Surface
import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
import androidx.camera.camera2.pipe.CameraPipe
import androidx.camera.camera2.pipe.CameraStream
import androidx.camera.camera2.pipe.CameraSurfaceManager
@@ -104,22 +106,25 @@
val pendingOutputs =
sessionFactory.create(
AndroidCameraDevice(
- testCamera.metadata, testCamera.cameraDevice, testCamera.cameraId),
+ testCamera.metadata, testCamera.cameraDevice, testCamera.cameraId
+ ),
mapOf(stream1.id to surface),
captureSessionState =
- CaptureSessionState(
- FakeGraphProcessor(),
- sessionFactory,
- object : Camera2CaptureSequenceProcessorFactory {
- override fun create(
- session: CameraCaptureSessionWrapper,
- surfaceMap: Map<StreamId, Surface>
- ): CaptureSequenceProcessor<Request, FakeCaptureSequence> =
- FakeCaptureSequenceProcessor()
- },
- CameraSurfaceManager(),
- SystemTimeSource(),
- this))
+ CaptureSessionState(
+ FakeGraphProcessor(),
+ sessionFactory,
+ object : Camera2CaptureSequenceProcessorFactory {
+ override fun create(
+ session: CameraCaptureSessionWrapper,
+ surfaceMap: Map<StreamId, Surface>
+ ): CaptureSequenceProcessor<Request, FakeCaptureSequence> =
+ FakeCaptureSequenceProcessor()
+ },
+ CameraSurfaceManager(),
+ SystemTimeSource(),
+ this
+ )
+ )
assertThat(pendingOutputs).isNotNull()
assertThat(pendingOutputs).isEmpty()
@@ -132,10 +137,12 @@
@Camera2ControllerScope
@Component(
modules =
- [
- FakeCameraGraphModule::class,
- FakeCameraPipeModule::class,
- Camera2CaptureSessionsModule::class])
+ [
+ FakeCameraGraphModule::class,
+ FakeCameraPipeModule::class,
+ Camera2CaptureSessionsModule::class,
+ FakeCamera2Module::class]
+)
internal interface Camera2CaptureSessionTestComponent {
fun graphConfig(): CameraGraph.Config
fun sessionFactory(): CaptureSessionFactory
@@ -148,9 +155,12 @@
private val context: Context,
private val fakeCamera: RobolectricCameras.FakeCamera
) {
- @Provides fun provideFakeCamera() = fakeCamera
+ @Provides
+ fun provideFakeCamera() = fakeCamera
- @Provides @Singleton fun provideFakeCameraPipeConfig() = CameraPipe.Config(context)
+ @Provides
+ @Singleton
+ fun provideFakeCameraPipeConfig() = CameraPipe.Config(context)
}
@Module(includes = [SharedCameraGraphModules::class])
@@ -169,3 +179,20 @@
)
}
}
+
+@Module
+class FakeCamera2Module {
+ @Provides
+ @Singleton
+ internal fun provideFakeCamera2MetadataProvider(
+ fakeCamera: RobolectricCameras.FakeCamera
+ ): Camera2MetadataProvider = object : Camera2MetadataProvider {
+ override suspend fun getCameraMetadata(cameraId: CameraId): CameraMetadata {
+ return fakeCamera.metadata
+ }
+
+ override fun awaitCameraMetadata(cameraId: CameraId): CameraMetadata {
+ return fakeCamera.metadata
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index 1056392..b2a19f8 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -63,6 +63,7 @@
import androidx.camera.core.impl.ImmediateSurface;
import androidx.camera.core.impl.Observable;
import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.testing.CameraUtil;
import androidx.camera.testing.HandlerUtil;
@@ -664,7 +665,8 @@
CameraSelector.LENS_FACING_BACK).build();
TestUseCase testUseCase = new TestUseCase(template, config,
selector, mMockOnImageAvailableListener, mMockRepeatingCaptureCallback);
- testUseCase.updateSuggestedResolution(new Size(640, 480));
+
+ testUseCase.updateSuggestedStreamSpec(StreamSpec.builder(new Size(640, 480)).build());
mFakeUseCases.add(testUseCase);
return testUseCase;
}
@@ -976,7 +978,7 @@
}
private void changeUseCaseSurface(UseCase useCase) {
- useCase.updateSuggestedResolution(new Size(640, 480));
+ useCase.updateSuggestedStreamSpec(StreamSpec.builder(new Size(640, 480)).build());
}
private void waitForCameraClose(Camera2CameraImpl camera2CameraImpl)
@@ -1035,7 +1037,7 @@
bindToCamera(new FakeCamera(mCameraId, null,
new FakeCameraInfoInternal(mCameraId, 0, lensFacing)),
null, null);
- updateSuggestedResolution(new Size(640, 480));
+ updateSuggestedStreamSpec(StreamSpec.builder(new Size(640, 480)).build());
}
public void close() {
@@ -1054,8 +1056,8 @@
@Override
@NonNull
- protected Size onSuggestedResolutionUpdated(
- @NonNull Size suggestedResolution) {
+ protected StreamSpec onSuggestedStreamSpecUpdated(
+ @NonNull StreamSpec suggestedStreamSpec) {
SessionConfig.Builder builder = SessionConfig.Builder.createFrom(mConfig);
builder.setTemplateType(mTemplate);
@@ -1064,6 +1066,7 @@
if (mDeferrableSurface != null) {
mDeferrableSurface.close();
}
+ Size suggestedResolution = suggestedStreamSpec.getResolution();
ImageReader imageReader =
ImageReader.newInstance(
suggestedResolution.getWidth(),
@@ -1080,7 +1083,7 @@
builder.addSurface(mDeferrableSurface);
updateSessionConfig(builder.build());
- return suggestedResolution;
+ return suggestedStreamSpec;
}
}
}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
index b895158..75e0fe5 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
@@ -51,7 +51,6 @@
import android.media.Image;
import android.media.ImageReader;
import android.media.ImageReader.OnImageAvailableListener;
-import android.media.MediaCodec;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
@@ -70,9 +69,6 @@
import androidx.camera.camera2.internal.compat.quirk.ConfigureSurfaceToSecondarySessionFailQuirk;
import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
import androidx.camera.camera2.internal.compat.quirk.PreviewOrientationIncorrectQuirk;
-import androidx.camera.core.ImageAnalysis;
-import androidx.camera.core.ImageCapture;
-import androidx.camera.core.Preview;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraCaptureCallbacks;
import androidx.camera.core.impl.CameraCaptureResult;
@@ -86,9 +82,7 @@
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.camera.testing.CameraUtil;
-import androidx.camera.testing.fakes.FakeUseCase;
import androidx.concurrent.futures.CallbackToFutureAdapter;
-import androidx.concurrent.futures.ResolvableFuture;
import androidx.core.os.HandlerCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -115,10 +109,8 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
-import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
@@ -159,16 +151,6 @@
private final List<CaptureSession> mCaptureSessions = new ArrayList<>();
private final List<DeferrableSurface> mDeferrableSurfaces = new ArrayList<>();
- DeferrableSurface mMockSurface = new DeferrableSurface() {
- private final ListenableFuture<Surface> mSurfaceFuture = ResolvableFuture.create();
- @NonNull
- @Override
- protected ListenableFuture<Surface> provideSurface() {
- // Return a never complete future.
- return mSurfaceFuture;
- }
- };
-
@Rule
public TestRule getUseCameraRule() {
if (SDK_INT >= 19) {
@@ -243,7 +225,6 @@
mTestParameters0.tearDown();
mTestParameters1.tearDown();
- mDeferrableSurfaces.add(mMockSurface);
for (DeferrableSurface deferrableSurface : mDeferrableSurfaces) {
deferrableSurface.close();
}
@@ -339,121 +320,6 @@
== CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW);
}
- @SdkSuppress(maxSdkVersion = 32, minSdkVersion = 21)
- @Test
- public void getStreamUseCaseFromUseCaseNotSupported() {
- Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
- mMockSurface.setContainerClass(Preview.class);
- StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
- new ArrayList<>(), streamUseCaseMap);
- assertTrue(streamUseCaseMap.isEmpty());
- }
-
- @SdkSuppress(minSdkVersion = 33)
- @Test
- public void getStreamUseCaseFromUseCaseEmptyUseCase() {
- Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
- StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
- new ArrayList<>(), streamUseCaseMap);
- assertTrue(streamUseCaseMap.isEmpty());
- }
-
- @SdkSuppress(minSdkVersion = 33)
- @Test
- public void getStreamUseCaseFromUseCaseNoPreview() {
- Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
- mMockSurface.setContainerClass(FakeUseCase.class);
- SessionConfig sessionConfig =
- new SessionConfig.Builder()
- .addSurface(mMockSurface).build();
- ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
- sessionConfigs.add(sessionConfig);
- StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
- sessionConfigs, streamUseCaseMap);
- assertTrue(streamUseCaseMap.isEmpty());
- }
-
- @SdkSuppress(minSdkVersion = 33)
- @Test
- public void getStreamUseCaseFromUseCasePreview() {
- Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
- mMockSurface.setContainerClass(Preview.class);
- SessionConfig sessionConfig =
- new SessionConfig.Builder()
- .addSurface(mMockSurface).build();
- ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
- sessionConfigs.add(sessionConfig);
- StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
- sessionConfigs, streamUseCaseMap);
- assertTrue(streamUseCaseMap.get(mMockSurface)
- == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW);
- }
-
- @SdkSuppress(minSdkVersion = 33)
- @Test
- public void getStreamUseCaseFromUseCaseZSL() {
- Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
- mMockSurface.setContainerClass(Preview.class);
- SessionConfig sessionConfig =
- new SessionConfig.Builder()
- .addSurface(mMockSurface)
- .setTemplateType(
- CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG).build();
- ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
- sessionConfigs.add(sessionConfig);
- StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
- sessionConfigs, streamUseCaseMap);
- assertTrue(streamUseCaseMap.isEmpty());
- }
-
- @SdkSuppress(minSdkVersion = 33)
- @Test
- public void getStreamUseCaseFromUseCaseImageAnalysis() {
- Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
- mMockSurface.setContainerClass(ImageAnalysis.class);
- SessionConfig sessionConfig =
- new SessionConfig.Builder()
- .addSurface(mMockSurface).build();
- ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
- sessionConfigs.add(sessionConfig);
- StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
- sessionConfigs, streamUseCaseMap);
- assertTrue(streamUseCaseMap.get(mMockSurface)
- == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW);
- }
-
- @SdkSuppress(minSdkVersion = 33)
- @Test
- public void getStreamUseCaseFromUseCaseConfigsImageCapture() {
- Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
- mMockSurface.setContainerClass(ImageCapture.class);
- SessionConfig sessionConfig =
- new SessionConfig.Builder()
- .addSurface(mMockSurface).build();
- ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
- sessionConfigs.add(sessionConfig);
- StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
- sessionConfigs, streamUseCaseMap);
- assertTrue(streamUseCaseMap.get(mMockSurface)
- == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_STILL_CAPTURE);
- }
-
- @SdkSuppress(minSdkVersion = 33)
- @Test
- public void getStreamUseCaseFromUseCaseConfigsVideoCapture() {
- Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
- mMockSurface.setContainerClass(MediaCodec.class);
- SessionConfig sessionConfig =
- new SessionConfig.Builder()
- .addSurface(mMockSurface).build();
- ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
- sessionConfigs.add(sessionConfig);
- StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
- sessionConfigs, streamUseCaseMap);
- assertTrue(streamUseCaseMap.get(mMockSurface)
- == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD);
- }
-
// Sharing surface of YUV format is supported since API 28
@SdkSuppress(minSdkVersion = 28)
@Test
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
index 51c93bb..0e6f94a 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
@@ -55,6 +55,7 @@
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.ImmediateSurface;
import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.internal.CameraUseCaseAdapter;
import androidx.camera.testing.CameraUtil;
@@ -167,8 +168,8 @@
FakeCameraDeviceSurfaceManager fakeCameraDeviceSurfaceManager =
new FakeCameraDeviceSurfaceManager();
- fakeCameraDeviceSurfaceManager.setSuggestedResolution(mCameraId, FakeUseCaseConfig.class,
- new Size(640, 480));
+ fakeCameraDeviceSurfaceManager.setSuggestedStreamSpec(mCameraId, FakeUseCaseConfig.class,
+ StreamSpec.builder(new Size(640, 480)).build());
mCameraUseCaseAdapter = new CameraUseCaseAdapter(
new LinkedHashSet<>(Collections.singleton(mCamera2CameraImpl)),
@@ -436,14 +437,14 @@
@Override
@NonNull
- protected Size onSuggestedResolutionUpdated(
- @NonNull Size suggestedResolution) {
- createPipeline(suggestedResolution);
+ protected StreamSpec onSuggestedStreamSpecUpdated(
+ @NonNull StreamSpec suggestedStreamSpec) {
+ createPipeline(suggestedStreamSpec);
notifyActive();
- return suggestedResolution;
+ return suggestedStreamSpec;
}
- private void createPipeline(Size resolution) {
+ private void createPipeline(StreamSpec streamSpec) {
SessionConfig.Builder builder = SessionConfig.Builder.createFrom(getCurrentConfig());
builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
@@ -452,6 +453,7 @@
}
// Create the metering DeferrableSurface
+ Size resolution = streamSpec.getResolution();
SurfaceTexture surfaceTexture = new SurfaceTexture(0);
surfaceTexture.setDefaultBufferSize(resolution.getWidth(), resolution.getHeight());
Surface surface = new Surface(surfaceTexture);
@@ -477,7 +479,7 @@
builder.addErrorListener((sessionConfig, error) -> {
// Create new pipeline and it will close the old one.
- createPipeline(resolution);
+ createPipeline(streamSpec);
});
updateSessionConfig(builder.build());
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
index db443b2..d14efa6 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
@@ -192,6 +192,9 @@
@NonNull
private final DisplayInfoManager mDisplayInfoManager;
+ @NonNull
+ private final CameraCharacteristicsCompat mCameraCharacteristicsCompat;
+
/**
* Constructor for a camera.
*
@@ -224,9 +227,9 @@
mCaptureSession = newCaptureSession();
try {
- CameraCharacteristicsCompat cameraCharacteristicsCompat =
+ mCameraCharacteristicsCompat =
mCameraManager.getCameraCharacteristicsCompat(cameraId);
- mCameraControlInternal = new Camera2CameraControlImpl(cameraCharacteristicsCompat,
+ mCameraControlInternal = new Camera2CameraControlImpl(mCameraCharacteristicsCompat,
mScheduledExecutorService, mExecutor, new ControlUpdateListenerInternal(),
cameraInfoImpl.getCameraQuirks());
mCameraInfoInternal = cameraInfoImpl;
@@ -1131,7 +1134,7 @@
Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
mUseCaseAttachState.getAttachedSessionConfigs(),
- streamUseCaseMap);
+ streamUseCaseMap, mCameraCharacteristicsCompat);
mCaptureSession.setStreamUseCaseMap(streamUseCaseMap);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
index 98e8758..c2e353ff 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
@@ -29,6 +29,7 @@
import androidx.camera.core.CameraUnavailableException;
import androidx.camera.core.impl.AttachedSurfaceInfo;
import androidx.camera.core.impl.CameraDeviceSurfaceManager;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.SurfaceConfig;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.core.util.Preconditions;
@@ -164,14 +165,14 @@
}
/**
- * Retrieves a map of suggested resolutions for the given list of use cases.
+ * Retrieves a map of suggested stream specifications for the given list of use cases.
*
* @param cameraId the camera id of the camera device used by the use cases
* @param existingSurfaces list of surfaces already configured and used by the camera. The
- * resolutions for these surface can not change.
+ * stream specifications for these surface can not change.
* @param newUseCaseConfigs list of configurations of the use cases that will be given a
- * suggested resolution
- * @return map of suggested resolutions for given use cases
+ * suggested stream specification
+ * @return map of suggested stream specifications for given use cases
* @throws IllegalStateException if not initialized
* @throws IllegalArgumentException if {@code newUseCaseConfigs} is an empty list, if
* there isn't a supported combination of surfaces
@@ -180,7 +181,7 @@
*/
@NonNull
@Override
- public Map<UseCaseConfig<?>, Size> getSuggestedResolutions(
+ public Map<UseCaseConfig<?>, StreamSpec> getSuggestedStreamSpecs(
@NonNull String cameraId,
@NonNull List<AttachedSurfaceInfo> existingSurfaces,
@NonNull List<UseCaseConfig<?>> newUseCaseConfigs) {
@@ -194,7 +195,7 @@
+ cameraId);
}
- return supportedSurfaceCombination.getSuggestedResolutions(existingSurfaces,
+ return supportedSurfaceCombination.getSuggestedStreamSpecifications(existingSurfaces,
newUseCaseConfigs);
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
index cb1a181..f2c6f0e 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
@@ -18,6 +18,7 @@
import static androidx.camera.camera2.impl.Camera2ImplConfig.STREAM_USE_CASE_OPTION;
+import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraMetadata;
import android.media.MediaCodec;
@@ -27,6 +28,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
@@ -37,7 +39,9 @@
import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
/**
* A class that contains utility methods for stream use case.
@@ -60,10 +64,23 @@
@OptIn(markerClass = ExperimentalCamera2Interop.class)
public static void populateSurfaceToStreamUseCaseMapping(
@NonNull Collection<SessionConfig> sessionConfigs,
- @NonNull Map<DeferrableSurface, Long> streamUseCaseMap) {
+ @NonNull Map<DeferrableSurface, Long> streamUseCaseMap,
+ @NonNull CameraCharacteristicsCompat cameraCharacteristicsCompat) {
if (Build.VERSION.SDK_INT < 33) {
return;
}
+
+ if (cameraCharacteristicsCompat.get(
+ CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES) == null) {
+ return;
+ }
+
+ Set<Long> supportedStreamUseCases = new HashSet<>();
+ for (long useCase : cameraCharacteristicsCompat.get(
+ CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES)) {
+ supportedStreamUseCases.add(useCase);
+ }
+
for (SessionConfig sessionConfig : sessionConfigs) {
if (sessionConfig.getTemplateType()
== CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG
@@ -73,34 +90,49 @@
return;
}
for (DeferrableSurface surface : sessionConfig.getSurfaces()) {
- if (sessionConfig.getImplementationOptions().containsOption(STREAM_USE_CASE_OPTION)
- &&
- sessionConfig.getImplementationOptions()
- .retrieveOption(STREAM_USE_CASE_OPTION) != null
- ) {
- streamUseCaseMap.put(
- surface,
- sessionConfig.getImplementationOptions()
- .retrieveOption(STREAM_USE_CASE_OPTION));
-
+ if (sessionConfig.getImplementationOptions().containsOption(
+ STREAM_USE_CASE_OPTION) && putStreamUseCaseToMappingIfAvailable(
+ streamUseCaseMap,
+ surface,
+ sessionConfig.getImplementationOptions().retrieveOption(
+ STREAM_USE_CASE_OPTION),
+ supportedStreamUseCases)) {
continue;
}
- @Nullable Long flag = getUseCaseToStreamUseCaseMapping()
+ Long streamUseCase = getUseCaseToStreamUseCaseMapping()
.get(surface.getContainerClass());
- if (flag != null) {
- streamUseCaseMap.put(surface, flag);
- }
+ putStreamUseCaseToMappingIfAvailable(streamUseCaseMap,
+ surface,
+ streamUseCase,
+ supportedStreamUseCases);
}
}
}
+ private static boolean putStreamUseCaseToMappingIfAvailable(
+ Map<DeferrableSurface, Long> streamUseCaseMap,
+ DeferrableSurface surface,
+ @Nullable Long streamUseCase,
+ Set<Long> availableStreamUseCases) {
+ if (streamUseCase == null) {
+ return false;
+ }
+
+ if (!availableStreamUseCases.contains(streamUseCase)) {
+ return false;
+ }
+
+ streamUseCaseMap.put(surface, streamUseCase);
+ return true;
+ }
+
/**
* Returns the mapping between the container class of a surface and the StreamUseCase
* associated with that class. Refer to {@link UseCase} for the potential UseCase as the
* container class for a given surface.
*/
- @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
private static Map<Class<?>, Long> getUseCaseToStreamUseCaseMapping() {
if (sUseCaseToStreamUseCaseMapping == null) {
sUseCaseToStreamUseCaseMapping = new HashMap<>();
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index f3818b8..5d53087 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -63,6 +63,7 @@
import androidx.camera.core.ResolutionSelector;
import androidx.camera.core.impl.AttachedSurfaceInfo;
import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.SurfaceCombination;
import androidx.camera.core.impl.SurfaceConfig;
import androidx.camera.core.impl.SurfaceSizeDefinition;
@@ -105,7 +106,7 @@
private boolean mIsBurstCaptureSupported = false;
@VisibleForTesting
SurfaceSizeDefinition mSurfaceSizeDefinition;
- private Map<Integer, Size[]> mOutputSizesCache = new HashMap<>();
+ private final Map<Integer, Size[]> mOutputSizesCache = new HashMap<>();
@NonNull
private final DisplayInfoManager mDisplayInfoManager;
private final ResolutionCorrector mResolutionCorrector = new ResolutionCorrector();
@@ -209,18 +210,18 @@
}
/**
- * Finds the suggested resolutions of the newly added UseCaseConfig.
+ * Finds the suggested stream specifications of the newly added UseCaseConfig.
*
* @param existingSurfaces the existing surfaces.
* @param newUseCaseConfigs newly added UseCaseConfig.
- * @return the suggested resolutions, which is a mapping from UseCaseConfig to the suggested
- * resolution.
+ * @return the suggested stream specifications, which is a mapping from UseCaseConfig to the
+ * suggested stream specification.
* @throws IllegalArgumentException if the suggested solution for newUseCaseConfigs cannot be
* found. This may be due to no available output size or no
* available surface combination.
*/
@NonNull
- Map<UseCaseConfig<?>, Size> getSuggestedResolutions(
+ Map<UseCaseConfig<?>, StreamSpec> getSuggestedStreamSpecifications(
@NonNull List<AttachedSurfaceInfo> existingSurfaces,
@NonNull List<UseCaseConfig<?>> newUseCaseConfigs) {
// Refresh Preview Size based on current display configurations.
@@ -265,7 +266,7 @@
getAllPossibleSizeArrangements(
supportedOutputSizesList);
- Map<UseCaseConfig<?>, Size> suggestedResolutionsMap = null;
+ Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecMap = null;
// Transform use cases to SurfaceConfig list and find the first (best) workable combination
for (List<Size> possibleSizeList : allPossibleSizeArrangements) {
// Attach SurfaceConfig of original use cases since it will impact the new use cases
@@ -286,18 +287,17 @@
// Check whether the SurfaceConfig combination can be supported
if (checkSupported(surfaceConfigList)) {
- suggestedResolutionsMap = new HashMap<>();
+ suggestedStreamSpecMap = new HashMap<>();
for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
- suggestedResolutionsMap.put(
+ suggestedStreamSpecMap.put(
useCaseConfig,
- possibleSizeList.get(
- useCasesPriorityOrder.indexOf(
- newUseCaseConfigs.indexOf(useCaseConfig))));
+ StreamSpec.builder(possibleSizeList.get(useCasesPriorityOrder.indexOf(
+ newUseCaseConfigs.indexOf(useCaseConfig)))).build());
}
break;
}
}
- if (suggestedResolutionsMap == null) {
+ if (suggestedStreamSpecMap == null) {
throw new IllegalArgumentException(
"No supported surface combination is found for camera device - Id : "
+ mCameraId + " and Hardware level: " + mHardwareLevel
@@ -305,7 +305,7 @@
+ " Existing surfaces: " + existingSurfaces
+ " New configs: " + newUseCaseConfigs);
}
- return suggestedResolutionsMap;
+ return suggestedStreamSpecMap;
}
/**
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java
new file mode 100644
index 0000000..f245ae2
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import static junit.framework.TestCase.assertTrue;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraMetadata;
+import android.media.MediaCodec;
+import android.os.Build;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.camera.camera2.impl.Camera2ImplConfig;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.Preview;
+import androidx.camera.core.impl.DeferrableSurface;
+import androidx.camera.core.impl.MutableOptionsBundle;
+import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.testing.fakes.FakeUseCase;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.test.filters.SdkSuppress;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowCameraCharacteristics;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class StreamUseCaseTest {
+
+ private CameraCharacteristics mCameraCharacteristics;
+
+ DeferrableSurface mMockSurface = new DeferrableSurface() {
+ private final ListenableFuture<Surface> mSurfaceFuture = ResolvableFuture.create();
+
+ @NonNull
+ @Override
+ protected ListenableFuture<Surface> provideSurface() {
+ // Return a never complete future.
+ return mSurfaceFuture;
+ }
+ };
+
+ @Before
+ public void setup() {
+ mCameraCharacteristics = ShadowCameraCharacteristics.newCameraCharacteristics();
+ }
+
+ @After
+ public void tearDown() {
+ mMockSurface.close();
+ }
+
+ @SdkSuppress(maxSdkVersion = 32, minSdkVersion = 21)
+ @Test
+ public void getStreamUseCaseFromOsNotSupported() {
+ Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+ mMockSurface.setContainerClass(Preview.class);
+ StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+ new ArrayList<>(), streamUseCaseMap, getCameraCharacteristicsCompat());
+ assertTrue(streamUseCaseMap.isEmpty());
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void getStreamUseCaseFromUseCaseEmptyUseCase() {
+ Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+ StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+ new ArrayList<>(), streamUseCaseMap, getCameraCharacteristicsCompat());
+ assertTrue(streamUseCaseMap.isEmpty());
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void getStreamUseCaseFromUseCaseNoPreview() {
+ Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+ mMockSurface.setContainerClass(FakeUseCase.class);
+ SessionConfig sessionConfig =
+ new SessionConfig.Builder()
+ .addSurface(mMockSurface).build();
+ ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+ sessionConfigs.add(sessionConfig);
+ StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+ sessionConfigs, streamUseCaseMap, getCameraCharacteristicsCompat());
+ assertTrue(streamUseCaseMap.isEmpty());
+ }
+
+ @Test
+ public void getStreamUseCaseFromUseCasePreview() {
+ Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+ mMockSurface.setContainerClass(Preview.class);
+ SessionConfig sessionConfig =
+ new SessionConfig.Builder()
+ .addSurface(mMockSurface).build();
+ ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+ sessionConfigs.add(sessionConfig);
+ StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+ sessionConfigs, streamUseCaseMap, getCameraCharacteristicsCompat());
+ assertTrue(streamUseCaseMap.get(mMockSurface)
+ == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW);
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void getStreamUseCaseFromUseCaseZSL() {
+ Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+ mMockSurface.setContainerClass(Preview.class);
+ SessionConfig sessionConfig =
+ new SessionConfig.Builder()
+ .addSurface(mMockSurface)
+ .setTemplateType(
+ CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG).build();
+ ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+ sessionConfigs.add(sessionConfig);
+ StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+ sessionConfigs, streamUseCaseMap, getCameraCharacteristicsCompat());
+ assertTrue(streamUseCaseMap.isEmpty());
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void getStreamUseCaseFromUseCaseImageAnalysis() {
+ Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+ mMockSurface.setContainerClass(ImageAnalysis.class);
+ SessionConfig sessionConfig =
+ new SessionConfig.Builder()
+ .addSurface(mMockSurface).build();
+ ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+ sessionConfigs.add(sessionConfig);
+ StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+ sessionConfigs, streamUseCaseMap, getCameraCharacteristicsCompat());
+ assertTrue(streamUseCaseMap.get(mMockSurface)
+ == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW);
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void getStreamUseCaseFromUseCaseConfigsImageCapture() {
+ Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+ mMockSurface.setContainerClass(ImageCapture.class);
+ SessionConfig sessionConfig =
+ new SessionConfig.Builder()
+ .addSurface(mMockSurface).build();
+ ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+ sessionConfigs.add(sessionConfig);
+ StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+ sessionConfigs, streamUseCaseMap, getCameraCharacteristicsCompat());
+ assertTrue(streamUseCaseMap.get(mMockSurface)
+ == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_STILL_CAPTURE);
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void getStreamUseCaseFromUseCaseConfigsVideoCapture() {
+ Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+ mMockSurface.setContainerClass(MediaCodec.class);
+ SessionConfig sessionConfig =
+ new SessionConfig.Builder()
+ .addSurface(mMockSurface).build();
+ ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+ sessionConfigs.add(sessionConfig);
+ StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+ sessionConfigs, streamUseCaseMap, getCameraCharacteristicsCompat());
+ assertTrue(streamUseCaseMap.get(mMockSurface)
+ == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD);
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void getStreamUseCaseWithNullAvailableUseCases() {
+ Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+ mMockSurface.setContainerClass(FakeUseCase.class);
+ SessionConfig sessionConfig =
+ new SessionConfig.Builder()
+ .addSurface(mMockSurface).build();
+ ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+ sessionConfigs.add(sessionConfig);
+ StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+ sessionConfigs, streamUseCaseMap,
+ CameraCharacteristicsCompat.toCameraCharacteristicsCompat(mCameraCharacteristics));
+ assertTrue(streamUseCaseMap.isEmpty());
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void getStreamUseCaseWithEmptyAvailableUseCases() {
+ Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+ mMockSurface.setContainerClass(Preview.class);
+ SessionConfig sessionConfig =
+ new SessionConfig.Builder()
+ .addSurface(mMockSurface).build();
+ ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+ sessionConfigs.add(sessionConfig);
+ StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+ sessionConfigs,
+ streamUseCaseMap,
+ getCameraCharacteristicsCompatWithEmptyUseCases());
+ assertTrue(streamUseCaseMap.isEmpty());
+ }
+
+ @Test
+ public void getStreamUseCaseFromCamera2Interop() {
+ Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+ mMockSurface.setContainerClass(Preview.class);
+ MutableOptionsBundle testStreamUseCaseConfig = MutableOptionsBundle.create();
+ testStreamUseCaseConfig.insertOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION, 3L);
+ SessionConfig sessionConfig =
+ new SessionConfig.Builder()
+ .addSurface(mMockSurface).addImplementationOptions(
+ testStreamUseCaseConfig).build();
+ ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+ sessionConfigs.add(sessionConfig);
+ StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+ sessionConfigs, streamUseCaseMap, getCameraCharacteristicsCompat());
+ assertTrue(streamUseCaseMap.get(mMockSurface) == 3L);
+ }
+
+ @Test
+ public void getUnsupportedStreamUseCaseFromCamera2Interop() {
+ Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
+ mMockSurface.setContainerClass(Preview.class);
+ MutableOptionsBundle testStreamUseCaseConfig = MutableOptionsBundle.create();
+ testStreamUseCaseConfig.insertOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION, -1L);
+ SessionConfig sessionConfig =
+ new SessionConfig.Builder()
+ .addSurface(mMockSurface).addImplementationOptions(
+ testStreamUseCaseConfig).build();
+ ArrayList<SessionConfig> sessionConfigs = new ArrayList<>();
+ sessionConfigs.add(sessionConfig);
+ StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
+ sessionConfigs, streamUseCaseMap, getCameraCharacteristicsCompat());
+ assertTrue(streamUseCaseMap.get(mMockSurface)
+ == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW);
+ }
+
+ private CameraCharacteristicsCompat getCameraCharacteristicsCompat() {
+ ShadowCameraCharacteristics shadowCharacteristics0 = Shadow.extract(mCameraCharacteristics);
+ if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ long[] uc = new long[]{CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT,
+ CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW,
+ CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL,
+ CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_STILL_CAPTURE,
+ CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL,
+ CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD};
+ shadowCharacteristics0.set(CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES, uc);
+ }
+ return CameraCharacteristicsCompat.toCameraCharacteristicsCompat(
+ mCameraCharacteristics);
+ }
+
+ private CameraCharacteristicsCompat getCameraCharacteristicsCompatWithEmptyUseCases() {
+ ShadowCameraCharacteristics shadowCharacteristics0 = Shadow.extract(mCameraCharacteristics);
+ if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ shadowCharacteristics0.set(CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES,
+ new long[]{});
+ }
+ return CameraCharacteristicsCompat.toCameraCharacteristicsCompat(
+ mCameraCharacteristics);
+ }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
index 2c2a694..3a0fcb1 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
@@ -45,6 +45,7 @@
import androidx.camera.core.impl.CameraFactory
import androidx.camera.core.impl.MutableStateObservable
import androidx.camera.core.impl.SizeCoordinate
+import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.SurfaceCombination
import androidx.camera.core.impl.SurfaceConfig
import androidx.camera.core.impl.SurfaceConfig.ConfigSize
@@ -510,9 +511,9 @@
)
val maxJpegSize = supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.JPEG)
val maxJpegAspectRatio = Rational(maxJpegSize.width, maxJpegSize.height)
- val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
- val selectedSize = suggestedResolutionMap[useCase]
- val resultAspectRatio = Rational(selectedSize!!.width, selectedSize.height)
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
+ val selectedSize = suggestedStreamSpecMap[useCase]!!.resolution
+ val resultAspectRatio = Rational(selectedSize.width, selectedSize.height)
// The targetAspectRatio value will only be set to the same aspect ratio as maximum
// supported jpeg size in Legacy + API 21 combination. For other combinations, it should
// keep the original targetAspectRatio set for the use case.
@@ -570,13 +571,13 @@
)
val maxJpegSize = supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.JPEG)
val maxJpegAspectRatio = Rational(maxJpegSize.width, maxJpegSize.height)
- val suggestedResolutionMap = getSuggestedResolutionMap(
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
supportedSurfaceCombination, preview,
imageCapture, imageAnalysis
)
- val previewResolution = suggestedResolutionMap[preview]
- val imageCaptureResolution = suggestedResolutionMap[imageCapture]
- val imageAnalysisResolution = suggestedResolutionMap[imageAnalysis]
+ val previewResolution = suggestedStreamSpecMap[preview]!!.resolution
+ val imageCaptureResolution = suggestedStreamSpecMap[imageCapture]!!.resolution
+ val imageAnalysisResolution = suggestedStreamSpecMap[imageAnalysis]!!.resolution
// The targetAspectRatio value will only be set to the same aspect ratio as maximum
// supported jpeg size in Legacy + API 21 combination. For other combinations, it should
// keep the original targetAspectRatio set for the use case.
@@ -584,16 +585,16 @@
// Checks targetAspectRatio and maxJpegAspectRatio, which is the ratio of maximum size
// in the mSupportedSizes, are not equal to make sure this test case is valid.
assertThat(targetAspectRatio).isNotEqualTo(maxJpegAspectRatio)
- assertThat(hasMatchingAspectRatio(previewResolution!!, maxJpegAspectRatio)).isTrue()
+ assertThat(hasMatchingAspectRatio(previewResolution, maxJpegAspectRatio)).isTrue()
assertThat(
hasMatchingAspectRatio(
- imageCaptureResolution!!,
+ imageCaptureResolution,
maxJpegAspectRatio
)
).isTrue()
assertThat(
hasMatchingAspectRatio(
- imageAnalysisResolution!!,
+ imageAnalysisResolution,
maxJpegAspectRatio
)
).isTrue()
@@ -601,19 +602,19 @@
// Checks no correction is needed.
assertThat(
hasMatchingAspectRatio(
- previewResolution!!,
+ previewResolution,
targetAspectRatio
)
).isTrue()
assertThat(
hasMatchingAspectRatio(
- imageCaptureResolution!!,
+ imageCaptureResolution,
targetAspectRatio
)
).isTrue()
assertThat(
hasMatchingAspectRatio(
- imageAnalysisResolution!!,
+ imageAnalysisResolution,
targetAspectRatio
)
).isTrue()
@@ -654,13 +655,13 @@
// Preview/ImageCapture/ImageAnalysis' default config settings that will be applied after
// bound to lifecycle. Calling bindToLifecycle here to make sure sizes matching to
// default aspect ratio will be selected.
- val suggestedResolutionMap = getSuggestedResolutionMap(
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
supportedSurfaceCombination, preview,
imageCapture, imageAnalysis
)
- val previewSize = suggestedResolutionMap[preview]!!
- val imageCaptureSize = suggestedResolutionMap[imageCapture]!!
- val imageAnalysisSize = suggestedResolutionMap[imageAnalysis]!!
+ val previewSize = suggestedStreamSpecMap[preview]!!.resolution
+ val imageCaptureSize = suggestedStreamSpecMap[imageCapture]!!.resolution
+ val imageAnalysisSize = suggestedStreamSpecMap[imageAnalysis]!!.resolution
val previewAspectRatio = Rational(previewSize.width, previewSize.height)
val imageCaptureAspectRatio = Rational(imageCaptureSize.width, imageCaptureSize.height)
@@ -696,7 +697,7 @@
PREVIEW_USE_CASE,
targetResolution = Size(displayHeight, displayWidth)
)
- val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, preview)
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, preview)
// Checks the preconditions.
val preconditionSize = Size(256, 144)
val targetRatio = Rational(displayHeight, displayWidth)
@@ -705,7 +706,7 @@
assertThat(Rational(it.width, it.height)).isNotEqualTo(targetRatio)
}
// Checks the mechanism has filtered out the sizes which are smaller than default size 480p.
- val previewSize = suggestedResolutionMap[preview]
+ val previewSize = suggestedStreamSpecMap[preview]
assertThat(previewSize).isNotEqualTo(preconditionSize)
}
@@ -741,9 +742,9 @@
Surface.ROTATION_90,
preferredResolution = it
)
- val suggestedResolutionMap =
- getSuggestedResolutionMap(supportedSurfaceCombination, imageCapture)
- assertThat(it).isEqualTo(suggestedResolutionMap[imageCapture])
+ val suggestedStreamSpecMap =
+ getSuggestedStreamSpecMap(supportedSurfaceCombination, imageCapture)
+ assertThat(it).isEqualTo(suggestedStreamSpecMap[imageCapture]!!.resolution)
}
}
@@ -792,23 +793,23 @@
Surface.ROTATION_90,
preferredResolution = resolution
)
- val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
- assertThat(suggestedResolutionMap[useCase]).isEqualTo(expectedResult)
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
+ assertThat(suggestedStreamSpecMap[useCase]!!.resolution).isEqualTo(expectedResult)
}
@Test
- fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice_LegacyApi() {
- suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice(legacyUseCaseCreator)
+ fun suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice_LegacyApi() {
+ suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice(legacyUseCaseCreator)
}
@Test
- fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice_ResolutionSelector() {
- suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice(
+ fun suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice_ResolutionSelector() {
+ suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice(
resolutionSelectorUseCaseCreator
)
}
- private fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice(
+ private fun suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice(
useCaseCreator: UseCaseCreator
) {
setupCameraAndInitCameraX(
@@ -829,7 +830,7 @@
// An IllegalArgumentException will be thrown because a LEGACY level device can't support
// ImageCapture + VideoCapture + Preview
assertThrows(IllegalArgumentException::class.java) {
- getSuggestedResolutionMap(
+ getSuggestedStreamSpecMap(
supportedSurfaceCombination,
imageCapture,
videoCapture,
@@ -839,18 +840,18 @@
}
@Test
- fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice_LegacyApi() {
- suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice(legacyUseCaseCreator)
+ fun suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice_LegacyApi() {
+ suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice(legacyUseCaseCreator)
}
@Test
- fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice_ResolutionSelector() {
- suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice(
+ fun suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice_ResolutionSelector() {
+ suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice(
resolutionSelectorUseCaseCreator
)
}
- private fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice(
+ private fun suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice(
useCaseCreator: UseCaseCreator
) {
setupCameraAndInitCameraX(
@@ -871,21 +872,21 @@
// An IllegalArgumentException will be thrown because the VideoCapture requests to only
// support a RECORD size but the configuration can't be supported on a LEGACY level device.
assertThrows(IllegalArgumentException::class.java) {
- getSuggestedResolutionMap(supportedSurfaceCombination, videoCapture, preview)
+ getSuggestedStreamSpecMap(supportedSurfaceCombination, videoCapture, preview)
}
}
@Test
- fun getSuggestedResolutionsForMixedUseCaseInLimitedDevice_LegacyApi() {
- getSuggestedResolutionsForMixedUseCaseInLimitedDevice(legacyUseCaseCreator)
+ fun getsuggestedStreamSpecsForMixedUseCaseInLimitedDevice_LegacyApi() {
+ getsuggestedStreamSpecsForMixedUseCaseInLimitedDevice(legacyUseCaseCreator)
}
@Test
- fun getSuggestedResolutionsForMixedUseCaseInLimitedDevice_ResolutionSelector() {
- getSuggestedResolutionsForMixedUseCaseInLimitedDevice(resolutionSelectorUseCaseCreator)
+ fun getsuggestedStreamSpecsForMixedUseCaseInLimitedDevice_ResolutionSelector() {
+ getsuggestedStreamSpecsForMixedUseCaseInLimitedDevice(resolutionSelectorUseCaseCreator)
}
- private fun getSuggestedResolutionsForMixedUseCaseInLimitedDevice(
+ private fun getsuggestedStreamSpecsForMixedUseCaseInLimitedDevice(
useCaseCreator: UseCaseCreator
) {
setupCameraAndInitCameraX(
@@ -903,16 +904,16 @@
PREVIEW_USE_CASE,
preferredAspectRatio = AspectRatio.RATIO_16_9
)
- val suggestedResolutionMap = getSuggestedResolutionMap(
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
supportedSurfaceCombination,
imageCapture,
videoCapture,
preview
)
// (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
- assertThat(suggestedResolutionMap[imageCapture]).isEqualTo(RECORD_SIZE)
- assertThat(suggestedResolutionMap[videoCapture]).isEqualTo(RECORD_SIZE)
- assertThat(suggestedResolutionMap[preview]).isEqualTo(PREVIEW_SIZE)
+ assertThat(suggestedStreamSpecMap[imageCapture]!!.resolution).isEqualTo(RECORD_SIZE)
+ assertThat(suggestedStreamSpecMap[videoCapture]!!.resolution).isEqualTo(RECORD_SIZE)
+ assertThat(suggestedStreamSpecMap[preview]!!.resolution).isEqualTo(PREVIEW_SIZE)
}
// For the use case in b/230651237,
@@ -920,19 +921,19 @@
// VideoCapture should have higher priority to choose size than ImageCapture.
@Test
@Throws(CameraUnavailableException::class)
- fun getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage_LegacyApi() {
- getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage(legacyUseCaseCreator)
+ fun getsuggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage_LegacyApi() {
+ getsuggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage(legacyUseCaseCreator)
}
@Test
@Throws(CameraUnavailableException::class)
- fun getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage_ResolutionSelector() {
- getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage(
+ fun getsuggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage_ResolutionSelector() {
+ getsuggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage(
resolutionSelectorUseCaseCreator
)
}
- private fun getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage(
+ private fun getsuggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage(
useCaseCreator: UseCaseCreator
) {
setupCameraAndInitCameraX(
@@ -953,7 +954,7 @@
PREVIEW_USE_CASE,
preferredAspectRatio = AspectRatio.RATIO_16_9
)
- val suggestedResolutionMap = getSuggestedResolutionMap(
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
supportedSurfaceCombination,
imageCapture,
videoCapture,
@@ -962,9 +963,9 @@
// There are two possible combinations in Full level device
// (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD) => should be applied
// (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM)
- assertThat(suggestedResolutionMap[imageCapture]).isEqualTo(RECORD_SIZE)
- assertThat(suggestedResolutionMap[videoCapture]).isEqualTo(RECORD_SIZE)
- assertThat(suggestedResolutionMap[preview]).isEqualTo(PREVIEW_SIZE)
+ assertThat(suggestedStreamSpecMap[imageCapture]!!.resolution).isEqualTo(RECORD_SIZE)
+ assertThat(suggestedStreamSpecMap[videoCapture]!!.resolution).isEqualTo(RECORD_SIZE)
+ assertThat(suggestedStreamSpecMap[preview]!!.resolution).isEqualTo(PREVIEW_SIZE)
}
@Test
@@ -1001,7 +1002,7 @@
PREVIEW_USE_CASE,
preferredAspectRatio = AspectRatio.RATIO_16_9
)
- val suggestedResolutionMap = getSuggestedResolutionMap(
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
supportedSurfaceCombination,
imageCapture,
videoCapture,
@@ -1010,36 +1011,37 @@
// There are two possible combinations in Full level device
// (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
// (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM) => should be applied
- assertThat(suggestedResolutionMap[imageCapture]).isEqualTo(MAXIMUM_SIZE)
- assertThat(suggestedResolutionMap[videoCapture]).isEqualTo(PREVIEW_SIZE) // Quality.HD
- assertThat(suggestedResolutionMap[preview]).isEqualTo(PREVIEW_SIZE)
+ assertThat(suggestedStreamSpecMap[imageCapture]!!.resolution).isEqualTo(MAXIMUM_SIZE)
+ // Quality.HD
+ assertThat(suggestedStreamSpecMap[videoCapture]!!.resolution).isEqualTo(PREVIEW_SIZE)
+ assertThat(suggestedStreamSpecMap[preview]!!.resolution).isEqualTo(PREVIEW_SIZE)
}
@Test
- fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases_LegacyApi() {
- getSuggestedResolutionsWithSameSupportedListForDifferentUseCases(
+ fun getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases_LegacyApi() {
+ getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases(
legacyUseCaseCreator,
DISPLAY_SIZE
)
}
@Test
- fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases_RS_SensorSize() {
- getSuggestedResolutionsWithSameSupportedListForDifferentUseCases(
+ fun getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases_RS_SensorSize() {
+ getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases(
resolutionSelectorUseCaseCreator,
PREVIEW_SIZE
)
}
@Test
- fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases_RS_ViewSize() {
- getSuggestedResolutionsWithSameSupportedListForDifferentUseCases(
+ fun getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases_RS_ViewSize() {
+ getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases(
viewSizeResolutionSelectorUseCaseCreator,
PREVIEW_SIZE
)
}
- private fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases(
+ private fun getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases(
useCaseCreator: UseCaseCreator,
preferredResolution: Size
) {
@@ -1082,15 +1084,15 @@
IMAGE_ANALYSIS_USE_CASE,
preferredResolution = preferredResolution
)
- val suggestedResolutionMap = getSuggestedResolutionMap(
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
supportedSurfaceCombination,
imageCapture,
imageAnalysis,
preview
)
- assertThat(suggestedResolutionMap[imageCapture]).isEqualTo(PREVIEW_SIZE)
- assertThat(suggestedResolutionMap[imageAnalysis]).isEqualTo(PREVIEW_SIZE)
- assertThat(suggestedResolutionMap[preview]).isEqualTo(PREVIEW_SIZE)
+ assertThat(suggestedStreamSpecMap[imageCapture]!!.resolution).isEqualTo(PREVIEW_SIZE)
+ assertThat(suggestedStreamSpecMap[imageAnalysis]!!.resolution).isEqualTo(PREVIEW_SIZE)
+ assertThat(suggestedStreamSpecMap[preview]!!.resolution).isEqualTo(PREVIEW_SIZE)
}
@Test
@@ -1122,7 +1124,7 @@
IMAGE_ANALYSIS_USE_CASE,
preferredAspectRatio = AspectRatio.RATIO_16_9
)
- val suggestedResolutionMap = getSuggestedResolutionMap(
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
supportedSurfaceCombination,
preview,
imageCapture,
@@ -1130,35 +1132,35 @@
)
assertThat(
hasMatchingAspectRatio(
- suggestedResolutionMap[preview]!!,
+ suggestedStreamSpecMap[preview]!!.resolution,
ASPECT_RATIO_16_9
)
).isTrue()
assertThat(
hasMatchingAspectRatio(
- suggestedResolutionMap[imageCapture]!!,
+ suggestedStreamSpecMap[imageCapture]!!.resolution,
ASPECT_RATIO_16_9
)
).isTrue()
assertThat(
hasMatchingAspectRatio(
- suggestedResolutionMap[imageAnalysis]!!,
+ suggestedStreamSpecMap[imageAnalysis]!!.resolution,
ASPECT_RATIO_16_9
)
).isTrue()
}
@Test
- fun getSuggestedResolutionsForCustomizedSupportedResolutions_LegacyApi() {
- getSuggestedResolutionsForCustomizedSupportedResolutions(legacyUseCaseCreator)
+ fun getsuggestedStreamSpecsForCustomizedSupportedResolutions_LegacyApi() {
+ getsuggestedStreamSpecsForCustomizedSupportedResolutions(legacyUseCaseCreator)
}
@Test
- fun getSuggestedResolutionsForCustomizedSupportedResolutions_ResolutionSelector() {
- getSuggestedResolutionsForCustomizedSupportedResolutions(resolutionSelectorUseCaseCreator)
+ fun getsuggestedStreamSpecsForCustomizedSupportedResolutions_ResolutionSelector() {
+ getsuggestedStreamSpecsForCustomizedSupportedResolutions(resolutionSelectorUseCaseCreator)
}
- private fun getSuggestedResolutionsForCustomizedSupportedResolutions(
+ private fun getsuggestedStreamSpecsForCustomizedSupportedResolutions(
useCaseCreator: UseCaseCreator
) {
setupCameraAndInitCameraX(
@@ -1182,16 +1184,16 @@
PREVIEW_USE_CASE,
supportedResolutions = formatResolutionsPairList
)
- val suggestedResolutionMap = getSuggestedResolutionMap(
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
supportedSurfaceCombination,
imageCapture,
videoCapture,
preview
)
- // Checks all suggested resolutions will become 640x480.
- assertThat(suggestedResolutionMap[imageCapture]).isEqualTo(RESOLUTION_VGA)
- assertThat(suggestedResolutionMap[videoCapture]).isEqualTo(RESOLUTION_VGA)
- assertThat(suggestedResolutionMap[preview]).isEqualTo(RESOLUTION_VGA)
+ // Checks all resolutions in suggested stream specs will become 640x480.
+ assertThat(suggestedStreamSpecMap[imageCapture]?.resolution).isEqualTo(RESOLUTION_VGA)
+ assertThat(suggestedStreamSpecMap[videoCapture]?.resolution).isEqualTo(RESOLUTION_VGA)
+ assertThat(suggestedStreamSpecMap[preview]?.resolution).isEqualTo(RESOLUTION_VGA)
}
@Test
@@ -1334,8 +1336,8 @@
preferredAspectRatio = AspectRatio.RATIO_16_9,
preferredResolution = MOD16_SIZE
)
- val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
- assertThat(suggestedResolutionMap[useCase]).isEqualTo(MOD16_SIZE)
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
+ assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(MOD16_SIZE)
}
@Test
@@ -1826,9 +1828,9 @@
PREVIEW_USE_CASE,
maxResolution = MAXIMUM_SIZE
)
- val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
// Checks mMaximumSize is final selected for the use case.
- assertThat(suggestedResolutionMap[useCase]).isEqualTo(MAXIMUM_SIZE)
+ assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(MAXIMUM_SIZE)
}
@Test
@@ -2406,9 +2408,9 @@
targetRotation = Surface.ROTATION_90,
preferredResolution = RESOLUTION_VGA
)
- val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
// Checks 640x480 is final selected for the use case.
- assertThat(suggestedResolutionMap[useCase]).isEqualTo(RESOLUTION_VGA)
+ assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(RESOLUTION_VGA)
}
@Test
@@ -2438,9 +2440,9 @@
FAKE_USE_CASE,
maxResolution = DISPLAY_SIZE
)
- val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
// Checks 480x480 is final selected for the use case.
- assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(480, 480))
+ assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(Size(480, 480))
}
@Test
@@ -2502,13 +2504,13 @@
targetRotation = Surface.ROTATION_90,
preferredResolution = RECORD_SIZE
)
- val suggestedResolutionMap = getSuggestedResolutionMap(
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
supportedSurfaceCombination,
preview,
imageCapture,
imageAnalysis
)
- assertThat(suggestedResolutionMap[imageAnalysis]).isEqualTo(expectedResult)
+ assertThat(suggestedStreamSpecMap[imageAnalysis]?.resolution).isEqualTo(expectedResult)
}
@Test
@@ -2570,13 +2572,13 @@
targetRotation = Surface.ROTATION_90,
preferredResolution = RECORD_SIZE
)
- val suggestedResolutionMap = getSuggestedResolutionMap(
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
supportedSurfaceCombination,
preview,
imageCapture,
imageAnalysis
)
- assertThat(suggestedResolutionMap[imageAnalysis]).isEqualTo(RECORD_SIZE)
+ assertThat(suggestedStreamSpecMap[imageAnalysis]?.resolution).isEqualTo(RECORD_SIZE)
}
@Config(minSdk = Build.VERSION_CODES.M)
@@ -2594,10 +2596,10 @@
)
val useCase = createUseCaseByResolutionSelector(FAKE_USE_CASE, highResolutionEnabled = true)
- val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
// Checks 8000x6000 is final selected for the use case.
- assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(8000, 6000))
+ assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(Size(8000, 6000))
}
@Config(minSdk = Build.VERSION_CODES.M)
@@ -2612,10 +2614,10 @@
)
val useCase = createUseCaseByResolutionSelector(FAKE_USE_CASE, highResolutionEnabled = true)
- val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
// Checks 8000x6000 is final selected for the use case.
- assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(4032, 3024))
+ assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(Size(4032, 3024))
}
@Config(minSdk = Build.VERSION_CODES.M)
@@ -2634,10 +2636,10 @@
val useCase =
createUseCaseByResolutionSelector(FAKE_USE_CASE, preferredResolution = Size(8000, 6000))
- val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
// Checks 8000x6000 is final selected for the use case.
- assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(4032, 3024))
+ assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(Size(4032, 3024))
}
@Config(minSdk = Build.VERSION_CODES.M)
@@ -2659,10 +2661,10 @@
preferredAspectRatio = AspectRatio.RATIO_16_9,
highResolutionEnabled = true
)
- val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+ val suggestedStreamSpecMap = getSuggestedStreamSpecMap(supportedSurfaceCombination, useCase)
// Checks 8000x6000 is final selected for the use case.
- assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(8000, 4500))
+ assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(Size(8000, 4500))
}
/**
@@ -2781,30 +2783,30 @@
* Gets the suggested resolution map by the converted ResolutionSelector use case config which
* will also be converted when a use case is bound to the lifecycle.
*/
- private fun getSuggestedResolutionMap(
+ private fun getSuggestedStreamSpecMap(
supportedSurfaceCombination: SupportedSurfaceCombination,
vararg useCases: UseCase,
cameraFactory: CameraFactory = this.cameraFactory!!,
cameraId: String = DEFAULT_CAMERA_ID,
useCaseConfigFactory: UseCaseConfigFactory = this.useCaseConfigFactory!!
- ): Map<UseCase, Size?> {
+ ): Map<UseCase, StreamSpec?> {
// Generates the use case to new ResolutionSelector use case config map
val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
cameraFactory.getCamera(cameraId).cameraInfoInternal,
listOf(*useCases),
useCaseConfigFactory
)
- // Uses the use case config list to get suggested resolutions
- val useCaseConfigResolutionMap = supportedSurfaceCombination.getSuggestedResolutions(
- emptyList(),
- mutableListOf<UseCaseConfig<*>?>().apply { addAll(useCaseToConfigMap.values) }
+ // Uses the use case config list to get suggested stream specs
+ val useCaseConfigStreamSpecMap = supportedSurfaceCombination
+ .getSuggestedStreamSpecifications(emptyList(), mutableListOf<UseCaseConfig<*>?>()
+ .apply { addAll(useCaseToConfigMap.values) }
)
- val useCaseResolutionMap = mutableMapOf<UseCase, Size?>()
+ val useCaseStreamSpecMap = mutableMapOf<UseCase, StreamSpec?>()
// Maps the use cases to the suggestion resolutions
for (useCase in useCases) {
- useCaseResolutionMap[useCase] = useCaseConfigResolutionMap[useCaseToConfigMap[useCase]]
+ useCaseStreamSpecMap[useCase] = useCaseConfigStreamSpecMap[useCaseToConfigMap[useCase]]
}
- return useCaseResolutionMap
+ return useCaseStreamSpecMap
}
/**
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java
index e011f44..0b1fc01 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java
@@ -16,13 +16,12 @@
package androidx.camera.core;
-import android.util.Size;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
import androidx.camera.testing.fakes.FakeUseCase;
@@ -80,8 +79,8 @@
@Override
@NonNull
- protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
- return suggestedResolution;
+ protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
+ return suggestedStreamSpec;
}
/** Returns true if {@link #onUnbind()} has been called previously. */
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
index 5c09967..3ea1b92 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
@@ -40,6 +40,7 @@
import androidx.camera.core.impl.CameraCaptureMetaData;
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.ImageCaptureConfig;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.UseCaseConfigFactory;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.internal.CameraUseCaseAdapter;
@@ -96,9 +97,9 @@
FakeCameraDeviceSurfaceManager fakeCameraDeviceSurfaceManager =
new FakeCameraDeviceSurfaceManager();
- fakeCameraDeviceSurfaceManager.setSuggestedResolution("fakeCameraId",
+ fakeCameraDeviceSurfaceManager.setSuggestedStreamSpec("fakeCameraId",
ImageCaptureConfig.class,
- new Size(640, 480));
+ StreamSpec.builder(new Size(640, 480)).build());
UseCaseConfigFactory useCaseConfigFactory = new FakeUseCaseConfigFactory();
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
index 4ab66e3..16ee9c4c 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
@@ -28,6 +28,7 @@
import androidx.camera.core.impl.Config
import androidx.camera.core.impl.ImageOutputConfig
import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.UseCaseConfigFactory
import androidx.camera.core.internal.CameraUseCaseAdapter
import androidx.camera.testing.fakes.FakeCamera
@@ -145,7 +146,7 @@
"UseCase"
).useCaseConfig
val testUseCase = TestUseCase(config)
- testUseCase.updateSuggestedResolution(Size(640, 480))
+ testUseCase.updateSuggestedStreamSpec(TEST_STREAM_SPEC)
Truth.assertThat(testUseCase.attachedSurfaceResolution).isNotNull()
testUseCase.bindToCamera(mockCameraInternal!!, null, null)
testUseCase.unbindFromCamera(mockCameraInternal!!)
@@ -153,6 +154,19 @@
}
@Test
+ fun attachedStreamSpecCanBeReset_whenOnDetach() {
+ val config = FakeUseCaseConfig.Builder().setTargetName(
+ "UseCase"
+ ).useCaseConfig
+ val testUseCase = TestUseCase(config)
+ testUseCase.updateSuggestedStreamSpec(TEST_STREAM_SPEC)
+ Truth.assertThat(testUseCase.attachedStreamSpec).isNotNull()
+ testUseCase.bindToCamera(mockCameraInternal!!, null, null)
+ testUseCase.unbindFromCamera(mockCameraInternal!!)
+ Truth.assertThat(testUseCase.attachedStreamSpec).isNull()
+ }
+
+ @Test
fun viewPortCropRectCanBeReset_whenOnDetach() {
val config = FakeUseCaseConfig.Builder().setTargetName(
"UseCase"
@@ -259,10 +273,10 @@
FakeCameraInfoInternal(cameraId)
)
val fakeCameraDeviceSurfaceManager = FakeCameraDeviceSurfaceManager()
- fakeCameraDeviceSurfaceManager.setSuggestedResolution(
+ fakeCameraDeviceSurfaceManager.setSuggestedStreamSpec(
cameraId,
FakeUseCaseConfig::class.java,
- SURFACE_RESOLUTION
+ TEST_STREAM_SPEC
)
val useCaseConfigFactory: UseCaseConfigFactory = FakeUseCaseConfigFactory()
return CameraUseCaseAdapter(
@@ -285,12 +299,15 @@
notifyUpdated()
}
- override fun onSuggestedResolutionUpdated(suggestedResolution: Size): Size {
- return suggestedResolution
+ override fun onSuggestedStreamSpecUpdated(suggestedStreamSpec: StreamSpec): StreamSpec {
+ return suggestedStreamSpec
}
}
companion object {
private val SURFACE_RESOLUTION: Size by lazy { Size(640, 480) }
+ private val TEST_STREAM_SPEC: StreamSpec by lazy {
+ StreamSpec.builder(SURFACE_RESOLUTION).build()
+ }
}
}
\ No newline at end of file
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/ViewPortsTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/ViewPortsTest.kt
index 37e533d..5a63cf5 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/ViewPortsTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/ViewPortsTest.kt
@@ -686,10 +686,10 @@
// Arrange.
// Convert the sizes into a UseCase map.
val orderedUseCases: MutableList<UseCase> = ArrayList()
- val useCaseSizeMap = HashMap<UseCase?, Size?>().apply {
+ val useCaseStreamSpecMap = HashMap<UseCase?, StreamSpec?>().apply {
for (size in surfaceSizes) {
val fakeUseCase = FakeUseCaseConfig.Builder().build()
- put(fakeUseCase, size)
+ put(fakeUseCase, StreamSpec.builder(size).build())
orderedUseCases.add(fakeUseCase)
}
}
@@ -702,7 +702,7 @@
rotationDegrees,
scaleType,
layoutDirection,
- useCaseSizeMap
+ useCaseStreamSpecMap
)
// Assert.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
index fd28040..58183ca 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
@@ -81,7 +81,7 @@
*/
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @IntDef(flag = true, value = {PREVIEW, IMAGE_CAPTURE})
+ @IntDef(flag = true, value = {PREVIEW, VIDEO_CAPTURE, IMAGE_CAPTURE})
public @interface Targets {
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
index 0dac3fc..d41351c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
@@ -74,6 +74,7 @@
import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.OptionsBundle;
import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
import androidx.camera.core.impl.utils.Threads;
@@ -302,8 +303,9 @@
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
SessionConfig.Builder createPipeline(@NonNull String cameraId,
- @NonNull ImageAnalysisConfig config, @NonNull Size resolution) {
+ @NonNull ImageAnalysisConfig config, @NonNull StreamSpec streamSpec) {
Threads.checkMainThread();
+ Size resolution = streamSpec.getResolution();
Executor backgroundExecutor = Preconditions.checkNotNull(config.getBackgroundExecutor(
CameraXExecutors.highPriorityExecutor()));
@@ -387,7 +389,7 @@
if (isCurrentCamera(cameraId)) {
// Only reset the pipeline when the bound camera is the same.
SessionConfig.Builder errorSessionConfigBuilder = createPipeline(cameraId, config,
- resolution);
+ streamSpec);
updateSessionConfig(errorSessionConfigBuilder.build());
notifyReset();
@@ -726,14 +728,14 @@
@Override
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
- protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
+ protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
final ImageAnalysisConfig config = (ImageAnalysisConfig) getCurrentConfig();
SessionConfig.Builder sessionConfigBuilder = createPipeline(getCameraId(), config,
- suggestedResolution);
+ suggestedStreamSpec);
updateSessionConfig(sessionConfigBuilder.build());
- return suggestedResolution;
+ return suggestedStreamSpec;
}
/**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index d23ff83..d5f8971 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -105,6 +105,7 @@
import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.OptionsBundle;
import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
import androidx.camera.core.impl.utils.CameraOrientationUtil;
@@ -370,10 +371,10 @@
@UiThread
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
SessionConfig.Builder createPipeline(@NonNull String cameraId,
- @NonNull ImageCaptureConfig config, @NonNull Size resolution) {
+ @NonNull ImageCaptureConfig config, @NonNull StreamSpec streamSpec) {
checkMainThread();
if (isNodeEnabled()) {
- return createPipelineWithNode(cameraId, config, resolution);
+ return createPipelineWithNode(cameraId, config, streamSpec);
}
SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
@@ -382,6 +383,7 @@
}
// Setup the ImageReader to do processing
+ Size resolution = streamSpec.getResolution();
if (config.getImageReaderProxyProvider() != null) {
mImageReader =
new SafeCloseImageReaderProxy(
@@ -450,7 +452,7 @@
// to this use case so we don't need to do this check.
if (isCurrentCamera(cameraId)) {
// Only reset the pipeline when the bound camera is the same.
- mSessionConfigBuilder = createPipeline(cameraId, config, resolution);
+ mSessionConfigBuilder = createPipeline(cameraId, config, streamSpec);
if (mImageCaptureRequestProcessor != null) {
// Restore the unfinished requests to the created pipeline
@@ -1527,16 +1529,16 @@
@NonNull
@Override
@RestrictTo(Scope.LIBRARY_GROUP)
- protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
+ protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
mSessionConfigBuilder = createPipeline(getCameraId(),
- (ImageCaptureConfig) getCurrentConfig(), suggestedResolution);
+ (ImageCaptureConfig) getCurrentConfig(), suggestedStreamSpec);
updateSessionConfig(mSessionConfigBuilder.build());
// In order to speed up the take picture process, notifyActive at an early stage to
// attach the session capture callback to repeating and get capture result all the time.
notifyActive();
- return suggestedResolution;
+ return suggestedStreamSpec;
}
/**
@@ -1655,10 +1657,11 @@
@OptIn(markerClass = ExperimentalZeroShutterLag.class)
@MainThread
private SessionConfig.Builder createPipelineWithNode(@NonNull String cameraId,
- @NonNull ImageCaptureConfig config, @NonNull Size resolution) {
+ @NonNull ImageCaptureConfig config, @NonNull StreamSpec streamSpec) {
checkMainThread();
- Log.d(TAG, String.format("createPipelineWithNode(cameraId: %s, resolution: %s)",
- cameraId, resolution));
+ Log.d(TAG, String.format("createPipelineWithNode(cameraId: %s, streamSpec: %s)",
+ cameraId, streamSpec));
+ Size resolution = streamSpec.getResolution();
checkState(mImagePipeline == null);
mImagePipeline = new ImagePipeline(config, resolution, mCameraEffect);
@@ -1679,7 +1682,7 @@
if (isCurrentCamera(cameraId)) {
mTakePictureManager.pause();
clearPipelineWithNode(/*keepTakePictureManager=*/ true);
- mSessionConfigBuilder = createPipeline(cameraId, config, resolution);
+ mSessionConfigBuilder = createPipeline(cameraId, config, streamSpec);
updateSessionConfig(mSessionConfigBuilder.build());
notifyReset();
mTakePictureManager.resume();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 26d9e29..c9de0b5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -78,6 +78,7 @@
import androidx.camera.core.impl.OptionsBundle;
import androidx.camera.core.impl.PreviewConfig;
import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
@@ -209,11 +210,11 @@
@MainThread
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
SessionConfig.Builder createPipeline(@NonNull String cameraId, @NonNull PreviewConfig config,
- @NonNull Size resolution) {
+ @NonNull StreamSpec streamSpec) {
// Build pipeline with node if processor is set. Eventually we will move all the code to
// createPipelineWithNode.
if (mSurfaceProcessor != null) {
- return createPipelineWithNode(cameraId, config, resolution);
+ return createPipelineWithNode(cameraId, config, streamSpec);
}
checkMainThread();
@@ -222,8 +223,8 @@
// Close previous session's deferrable surface before creating new one
clearPipeline();
- final SurfaceRequest surfaceRequest = new SurfaceRequest(resolution, getCamera(),
- this::notifyReset);
+ final SurfaceRequest surfaceRequest = new SurfaceRequest(streamSpec.getResolution(),
+ getCamera(), this::notifyReset);
mCurrentSurfaceRequest = surfaceRequest;
if (mSurfaceProvider != null) {
@@ -232,7 +233,7 @@
}
mSessionDeferrableSurface = surfaceRequest.getDeferrableSurface();
- addCameraSurfaceAndErrorListener(sessionConfigBuilder, cameraId, config, resolution);
+ addCameraSurfaceAndErrorListener(sessionConfigBuilder, cameraId, config, streamSpec);
return sessionConfigBuilder;
}
@@ -247,7 +248,7 @@
private SessionConfig.Builder createPipelineWithNode(
@NonNull String cameraId,
@NonNull PreviewConfig config,
- @NonNull Size resolution) {
+ @NonNull StreamSpec streamSpec) {
// Check arguments
checkMainThread();
checkNotNull(mSurfaceProcessor);
@@ -262,10 +263,10 @@
checkState(mCameraEdge == null);
mCameraEdge = new SurfaceEdge(
PREVIEW,
- resolution,
+ streamSpec,
new Matrix(),
getHasCameraTransform(),
- requireNonNull(getCropRect(resolution)),
+ requireNonNull(getCropRect(streamSpec.getResolution())),
getRelativeRotation(camera),
shouldMirror());
mCameraEdge.addOnInvalidatedListener(this::notifyReset);
@@ -286,7 +287,7 @@
// Send the camera Surface to the camera2.
SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
- addCameraSurfaceAndErrorListener(sessionConfigBuilder, cameraId, config, resolution);
+ addCameraSurfaceAndErrorListener(sessionConfigBuilder, cameraId, config, streamSpec);
return sessionConfigBuilder;
}
@@ -360,7 +361,7 @@
@NonNull SessionConfig.Builder sessionConfigBuilder,
@NonNull String cameraId,
@NonNull PreviewConfig config,
- @NonNull Size resolution) {
+ @NonNull StreamSpec streamSpec) {
// TODO(b/245309800): Add the Surface if post-processing pipeline is used. Post-processing
// pipeline always provide a Surface.
@@ -379,7 +380,7 @@
if (isCurrentCamera(cameraId)) {
// Only reset the pipeline when the bound camera is the same.
SessionConfig.Builder sessionConfigBuilder1 = createPipeline(cameraId, config,
- resolution);
+ streamSpec);
updateSessionConfig(sessionConfigBuilder1.build());
notifyReset();
@@ -483,7 +484,7 @@
// config and let createPipeline() sends a new SurfaceRequest.
if (getAttachedSurfaceResolution() != null) {
updateConfigAndOutput(getCameraId(), (PreviewConfig) getCurrentConfig(),
- getAttachedSurfaceResolution());
+ getAttachedStreamSpec());
notifyReset();
}
}
@@ -515,8 +516,8 @@
}
private void updateConfigAndOutput(@NonNull String cameraId, @NonNull PreviewConfig config,
- @NonNull Size resolution) {
- updateSessionConfig(createPipeline(cameraId, config, resolution).build());
+ @NonNull StreamSpec streamSpec) {
+ updateSessionConfig(createPipeline(cameraId, config, streamSpec).build());
}
/**
@@ -647,11 +648,11 @@
@Override
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
- protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
- mSurfaceSize = suggestedResolution;
+ protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
+ mSurfaceSize = suggestedStreamSpec.getResolution();
updateConfigAndOutput(getCameraId(), (PreviewConfig) getCurrentConfig(),
- mSurfaceSize);
- return suggestedResolution;
+ suggestedStreamSpec);
+ return suggestedStreamSpec;
}
/**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index 9599a59..e37d862 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -41,6 +41,7 @@
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
import androidx.camera.core.internal.TargetConfig;
@@ -107,9 +108,9 @@
////////////////////////////////////////////////////////////////////////////////////////////
/**
- * The resolution assigned to the {@link UseCase} based on the attached camera.
+ * The {@link StreamSpec} assigned to the {@link UseCase} based on the attached camera.
*/
- private Size mAttachedResolution;
+ private StreamSpec mAttachedStreamSpec;
/**
* The camera implementation provided Config. Its options has lowest priority and will be
@@ -536,17 +537,29 @@
@RestrictTo(Scope.LIBRARY_GROUP)
@Nullable
public Size getAttachedSurfaceResolution() {
- return mAttachedResolution;
+ return mAttachedStreamSpec != null ? mAttachedStreamSpec.getResolution() : null;
}
/**
- * Offers suggested resolution for the UseCase.
+ * Retrieves the currently attached stream specification.
+ *
+ * @return the currently attached stream specification.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public StreamSpec getAttachedStreamSpec() {
+ return mAttachedStreamSpec;
+ }
+
+ /**
+ * Offers suggested stream specification for the UseCase.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
- public void updateSuggestedResolution(@NonNull Size suggestedResolution) {
- mAttachedResolution = onSuggestedResolutionUpdated(suggestedResolution);
+ public void updateSuggestedStreamSpec(@NonNull StreamSpec suggestedStreamSpec) {
+ mAttachedStreamSpec = onSuggestedStreamSpecUpdated(suggestedStreamSpec);
}
/**
@@ -554,18 +567,18 @@
* CameraSelector, UseCase...)}.
*
* <p>Override to create necessary objects like {@link ImageReader} depending
- * on the resolution.
+ * on the stream specification.
*
- * @param suggestedResolution The suggested resolution that depends on camera device
+ * @param suggestedStreamSpec The suggested stream specification that depends on camera device
* capability and what and how many use cases will be bound.
- * @return The resolution that finally used to create the SessionConfig to
+ * @return The stream specification that finally used to create the SessionConfig to
* attach to the camera device.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
- protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
- return suggestedResolution;
+ protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
+ return suggestedStreamSpec;
}
/**
@@ -633,7 +646,7 @@
* make the use case work correctly.
*
* <p>After this function is invoked, CameraX will also provide the selected resolution
- * information to subclasses via {@link #onSuggestedResolutionUpdated}. Subclasses should
+ * information to subclasses via {@link #onSuggestedStreamSpecUpdated}. Subclasses should
* override it to set up the pipeline according to the selected resolution, so that UseCase
* becomes ready to receive data from the camera.
*
@@ -677,7 +690,7 @@
mCamera = null;
}
- mAttachedResolution = null;
+ mAttachedStreamSpec = null;
mViewPortCropRect = null;
// Resets the mUseCaseConfig to the initial status when the use case was created to make
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/AttachedSurfaceInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/AttachedSurfaceInfo.java
index 3fb6ad59..9e6c442 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/AttachedSurfaceInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/AttachedSurfaceInfo.java
@@ -61,7 +61,6 @@
/** Returns the configuration target frame rate. */
@Nullable
public abstract Range<Integer> getTargetFrameRate();
-
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
index c77004a..5bacbf2 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
@@ -78,22 +78,22 @@
@NonNull Size size);
/**
- * Retrieves a map of suggested resolutions for the given list of use cases.
+ * Retrieves a map of suggested stream specifications for the given list of use cases.
*
* @param cameraId the camera id of the camera device used by the use cases
* @param existingSurfaces list of surfaces already configured and used by the camera. The
- * resolutions for these surface can not change.
+ * stream specifications for these surface can not change.
* @param newUseCaseConfigs list of configurations of the use cases that will be given a
- * suggested resolution
- * @return map of suggested resolutions for given use cases
- *
+ * suggested stream specification
+ * @return map of suggested stream specifications for given use cases
* @throws IllegalStateException if not initialized
* @throws IllegalArgumentException if {@code newUseCaseConfigs} is an empty list, if
- * there isn't a supported combination of surfaces available, or if the {@code cameraId}
- * is not a valid id.
+ * there isn't a supported combination of surfaces
+ * available, or if the {@code cameraId}
+ * is not a valid id.
*/
@NonNull
- Map<UseCaseConfig<?>, Size> getSuggestedResolutions(
+ Map<UseCaseConfig<?>, StreamSpec> getSuggestedStreamSpecs(
@NonNull String cameraId,
@NonNull List<AttachedSurfaceInfo> existingSurfaces,
@NonNull List<UseCaseConfig<?>> newUseCaseConfigs);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/StreamSpec.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/StreamSpec.java
new file mode 100644
index 0000000..932e71a
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/StreamSpec.java
@@ -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.camera.core.impl;
+
+import android.util.Range;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * A stream specification defining how a camera frame stream should be configured.
+ *
+ * <p>The values communicated by this class specify what the camera is expecting to produce as a
+ * frame stream, and can be useful for configuring the frame consumer.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@AutoValue
+public abstract class StreamSpec {
+
+ /** A frame rate range with no specified upper or lower bound. */
+ public static final Range<Integer> FRAME_RATE_RANGE_UNSPECIFIED = new Range<>(0, 0);
+
+ /**
+ * Returns the resolution for the stream associated with this stream specification.
+ * @return the resolution for the stream.
+ */
+ @NonNull
+ public abstract Size getResolution();
+
+ /**
+ * Returns the expected frame rate range for the stream associated with this stream
+ * specification.
+ * @return the expected frame rate range for the stream.
+ */
+ @NonNull
+ public abstract Range<Integer> getExpectedFrameRateRange();
+
+ /** Returns a build for a stream configuration that takes a required resolution. */
+ @NonNull
+ public static Builder builder(@NonNull Size resolution) {
+ return new AutoValue_StreamSpec.Builder()
+ .setResolution(resolution)
+ .setExpectedFrameRateRange(FRAME_RATE_RANGE_UNSPECIFIED);
+ }
+
+ /** Returns a builder pre-populated with the current specification. */
+ @NonNull
+ public abstract Builder toBuilder();
+
+ /** A builder for a stream specification */
+ @AutoValue.Builder
+ public abstract static class Builder {
+ // Restrict construction to same package
+ Builder() {
+ }
+
+ /** Sets the resolution, overriding the existing resolution set in this builder. */
+ @NonNull
+ public abstract Builder setResolution(@NonNull Size resolution);
+
+ /**
+ * Sets the expected frame rate range.
+ *
+ * <p>If not set, the default expected frame rate range is
+ * {@link #FRAME_RATE_RANGE_UNSPECIFIED}.
+ */
+ @NonNull
+ public abstract Builder setExpectedFrameRateRange(@NonNull Range<Integer> range);
+
+ /** Builds the stream specification */
+ @NonNull
+ public abstract StreamSpec build();
+ }
+
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index 14061ef..45d12bd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -53,6 +53,7 @@
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.SurfaceConfig;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
@@ -83,7 +84,7 @@
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public final class CameraUseCaseAdapter implements Camera {
@NonNull
- private CameraInternal mCameraInternal;
+ private final CameraInternal mCameraInternal;
private final LinkedHashSet<CameraInternal> mCameraInternals;
private final CameraDeviceSurfaceManager mCameraDeviceSurfaceManager;
private final UseCaseConfigFactory mUseCaseConfigFactory;
@@ -212,7 +213,6 @@
} catch (IllegalArgumentException e) {
throw new CameraException(e.getMessage());
}
-
}
}
@@ -267,9 +267,10 @@
// fails the supported stream combination rules.
Map<UseCase, ConfigPair> configs = getConfigs(cameraUseCasesToAttach,
mCameraConfig.getUseCaseConfigFactory(), mUseCaseConfigFactory);
- Map<UseCase, Size> suggestedResolutionsMap;
+
+ Map<UseCase, StreamSpec> suggestedStreamSpecMap;
try {
- suggestedResolutionsMap = calculateSuggestedResolutions(
+ suggestedStreamSpecMap = calculateSuggestedStreamSpecs(
mCameraInternal.getCameraInfoInternal(), cameraUseCasesToAttach,
cameraUseCasesToKeep, configs);
// TODO(b/265704882): enable stream sharing for LEVEL_3 and high preview
@@ -287,7 +288,7 @@
}
// Update properties.
- updateViewPort(suggestedResolutionsMap, cameraUseCases);
+ updateViewPort(suggestedStreamSpecMap, cameraUseCases);
updateEffects(mEffects, appUseCases);
// Detach unused UseCases.
@@ -301,8 +302,8 @@
ConfigPair configPair = requireNonNull(configs.get(useCase));
useCase.bindToCamera(mCameraInternal, configPair.mExtendedConfig,
configPair.mCameraConfig);
- useCase.updateSuggestedResolution(
- Preconditions.checkNotNull(suggestedResolutionsMap.get(useCase)));
+ useCase.updateSuggestedStreamSpec(
+ Preconditions.checkNotNull(suggestedStreamSpecMap.get(useCase)));
}
if (mAttached) {
mCameraInternal.attachUseCases(cameraUseCasesToAttach);
@@ -485,14 +486,14 @@
}
}
- private Map<UseCase, Size> calculateSuggestedResolutions(
+ private Map<UseCase, StreamSpec> calculateSuggestedStreamSpecs(
@NonNull CameraInfoInternal cameraInfoInternal,
@NonNull Collection<UseCase> newUseCases,
@NonNull Collection<UseCase> currentUseCases,
@NonNull Map<UseCase, ConfigPair> configPairMap) {
List<AttachedSurfaceInfo> existingSurfaces = new ArrayList<>();
String cameraId = cameraInfoInternal.getCameraId();
- Map<UseCase, Size> suggestedResolutions = new HashMap<>();
+ Map<UseCase, StreamSpec> suggestedStreamSpecs = new HashMap<>();
// Get resolution for current use cases.
for (UseCase useCase : currentUseCases) {
@@ -503,7 +504,7 @@
existingSurfaces.add(AttachedSurfaceInfo.create(surfaceConfig,
useCase.getImageFormat(), useCase.getAttachedSurfaceResolution(),
useCase.getCurrentConfig().getTargetFramerate(null)));
- suggestedResolutions.put(useCase, useCase.getAttachedSurfaceResolution());
+ suggestedStreamSpecs.put(useCase, useCase.getAttachedStreamSpec());
}
// Calculate resolution for new use cases.
@@ -518,17 +519,17 @@
configToUseCaseMap.put(combinedUseCaseConfig, useCase);
}
- // Get suggested resolutions and update the use case session configuration
- Map<UseCaseConfig<?>, Size> useCaseConfigSizeMap = mCameraDeviceSurfaceManager
- .getSuggestedResolutions(cameraId, existingSurfaces,
+ // Get suggested stream specifications and update the use case session configuration
+ Map<UseCaseConfig<?>, StreamSpec> useCaseConfigStreamSpecMap =
+ mCameraDeviceSurfaceManager.getSuggestedStreamSpecs(cameraId, existingSurfaces,
new ArrayList<>(configToUseCaseMap.keySet()));
for (Map.Entry<UseCaseConfig<?>, UseCase> entry : configToUseCaseMap.entrySet()) {
- suggestedResolutions.put(entry.getValue(),
- useCaseConfigSizeMap.get(entry.getKey()));
+ suggestedStreamSpecs.put(entry.getValue(),
+ useCaseConfigStreamSpecMap.get(entry.getKey()));
}
}
- return suggestedResolutions;
+ return suggestedStreamSpecs;
}
@VisibleForTesting
@@ -558,7 +559,7 @@
}
}
- private void updateViewPort(@NonNull Map<UseCase, Size> suggestedResolutionsMap,
+ private void updateViewPort(@NonNull Map<UseCase, StreamSpec> suggestedStreamSpecMap,
@NonNull Collection<UseCase> useCases) {
synchronized (mLock) {
if (mViewPort != null) {
@@ -582,14 +583,15 @@
mViewPort.getRotation()),
mViewPort.getScaleType(),
mViewPort.getLayoutDirection(),
- suggestedResolutionsMap);
+ suggestedStreamSpecMap);
for (UseCase useCase : useCases) {
useCase.setViewPortCropRect(
Preconditions.checkNotNull(cropRectMap.get(useCase)));
useCase.setSensorToBufferTransformMatrix(
calculateSensorToBufferTransformMatrix(
mCameraInternal.getCameraControlInternal().getSensorRect(),
- suggestedResolutionsMap.get(useCase)));
+ Preconditions.checkNotNull(
+ suggestedStreamSpecMap.get(useCase)).getResolution()));
}
}
}
@@ -739,7 +741,7 @@
try {
Map<UseCase, ConfigPair> configs = getConfigs(Arrays.asList(useCases),
mCameraConfig.getUseCaseConfigFactory(), mUseCaseConfigFactory);
- calculateSuggestedResolutions(mCameraInternal.getCameraInfoInternal(),
+ calculateSuggestedStreamSpecs(mCameraInternal.getCameraInfoInternal(),
Arrays.asList(useCases), emptyList(), configs);
} catch (IllegalArgumentException e) {
return false;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/ViewPorts.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/ViewPorts.java
index 8f39855..e20f0ed 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/ViewPorts.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/ViewPorts.java
@@ -22,13 +22,13 @@
import android.graphics.RectF;
import android.util.LayoutDirection;
import android.util.Rational;
-import android.util.Size;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.core.UseCase;
import androidx.camera.core.ViewPort;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.internal.utils.ImageUtil;
import androidx.core.util.Preconditions;
@@ -57,7 +57,7 @@
* rotation.
* @param scaleType The scale type to calculate
* @param layoutDirection The direction of layout.
- * @param useCaseSizes The resolutions of the UseCases
+ * @param useCaseStreamSpecs The stream specifications of the UseCases
* @return The set of Viewports that should be set for each UseCase
*/
@NonNull
@@ -68,7 +68,7 @@
@IntRange(from = 0, to = 359) int outputRotationDegrees,
@ViewPort.ScaleType int scaleType,
@ViewPort.LayoutDirection int layoutDirection,
- @NonNull Map<UseCase, Size> useCaseSizes) {
+ @NonNull Map<UseCase, StreamSpec> useCaseStreamSpecs) {
Preconditions.checkArgument(
fullSensorRect.width() > 0 && fullSensorRect.height() > 0,
"Cannot compute viewport crop rects zero sized sensor rect.");
@@ -82,11 +82,11 @@
RectF fullSensorRectF = new RectF(fullSensorRect);
Map<UseCase, Matrix> useCaseToSensorTransformations = new HashMap<>();
RectF sensorIntersectionRect = new RectF(fullSensorRect);
- for (Map.Entry<UseCase, Size> entry : useCaseSizes.entrySet()) {
+ for (Map.Entry<UseCase, StreamSpec> entry : useCaseStreamSpecs.entrySet()) {
// Calculate the transformation from UseCase to sensor.
Matrix useCaseToSensorTransformation = new Matrix();
- RectF srcRect = new RectF(0, 0, entry.getValue().getWidth(),
- entry.getValue().getHeight());
+ RectF srcRect = new RectF(0, 0, entry.getValue().getResolution().getWidth(),
+ entry.getValue().getResolution().getHeight());
useCaseToSensorTransformation.setRectToRect(srcRect, fullSensorRectF,
Matrix.ScaleToFit.CENTER);
useCaseToSensorTransformations.put(entry.getKey(), useCaseToSensorTransformation);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
index e1951631..ca3c2df 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
@@ -50,6 +50,7 @@
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.concurrent.futures.CallbackToFutureAdapter;
@@ -101,7 +102,7 @@
private final boolean mMirroring;
@CameraEffect.Targets
private final int mTargets;
- private final Size mSize;
+ private final StreamSpec mStreamSpec;
// Guarded by main thread.
private int mRotationDegrees;
@@ -128,20 +129,20 @@
*/
public SurfaceEdge(
@CameraEffect.Targets int targets,
- @NonNull Size size,
+ @NonNull StreamSpec streamSpec,
@NonNull Matrix sensorToBufferTransform,
boolean hasCameraTransform,
@NonNull Rect cropRect,
int rotationDegrees,
boolean mirroring) {
mTargets = targets;
- mSize = size;
+ mStreamSpec = streamSpec;
mSensorToBufferTransform = sensorToBufferTransform;
mHasCameraTransform = hasCameraTransform;
mCropRect = cropRect;
mRotationDegrees = rotationDegrees;
mMirroring = mirroring;
- mSettableSurface = new SettableSurface(size);
+ mSettableSurface = new SettableSurface(streamSpec.getResolution());
}
/**
@@ -248,8 +249,9 @@
@Nullable Range<Integer> expectedFpsRange) {
checkMainThread();
// TODO(b/238230154) figure out how to support HDR.
- SurfaceRequest surfaceRequest = new SurfaceRequest(getSize(), cameraInternal,
- expectedFpsRange, () -> mainThreadExecutor().execute(this::invalidate));
+ SurfaceRequest surfaceRequest = new SurfaceRequest(mStreamSpec.getResolution(),
+ cameraInternal, expectedFpsRange,
+ () -> mainThreadExecutor().execute(this::invalidate));
try {
DeferrableSurface deferrableSurface = surfaceRequest.getDeferrableSurface();
if (mSettableSurface.setProvider(deferrableSurface)) {
@@ -308,8 +310,8 @@
return immediateFailedFuture(e);
}
SurfaceOutputImpl surfaceOutputImpl = new SurfaceOutputImpl(surface,
- getTargets(), getSize(), inputSize, cropRect, rotationDegrees,
- mirroring);
+ getTargets(), mStreamSpec.getResolution(), inputSize, cropRect,
+ rotationDegrees, mirroring);
surfaceOutputImpl.getCloseFuture().addListener(
settableSurface::decrementUseCount,
directExecutor());
@@ -337,7 +339,7 @@
checkMainThread();
close();
mHasConsumer = false;
- mSettableSurface = new SettableSurface(mSize);
+ mSettableSurface = new SettableSurface(mStreamSpec.getResolution());
for (Runnable onInvalidated : mOnInvalidatedListeners) {
onInvalidated.run();
}
@@ -374,14 +376,6 @@
}
/**
- * The allocated size of the {@link Surface}.
- */
- @NonNull
- public Size getSize() {
- return mSize;
- }
-
- /**
* Gets the {@link Matrix} represents the transformation from camera sensor to the current
* {@link Surface}.
*
@@ -475,6 +469,14 @@
}
/**
+ * Returns {@link StreamSpec} associated with this edge.
+ */
+ @NonNull
+ public StreamSpec getStreamSpec() {
+ return mStreamSpec;
+ }
+
+ /**
* A {@link DeferrableSurface} that sets another {@link DeferrableSurface} as the source.
*
* <p>This class provides mechanisms to link an {@link DeferrableSurface}, and propagates
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index aaf7415..286ddca 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -40,6 +40,7 @@
import androidx.camera.core.SurfaceRequest;
import androidx.camera.core.UseCase;
import androidx.camera.core.impl.CameraInternal;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.utils.Threads;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
@@ -121,9 +122,7 @@
@NonNull
private SurfaceEdge transformSingleOutput(@NonNull SurfaceEdge input,
@NonNull OutConfig outConfig) {
-
SurfaceEdge outputSurface;
- Size inputSize = input.getSize();
Rect cropRect = outConfig.getCropRect();
int rotationDegrees = input.getRotationDegrees();
boolean mirroring = outConfig.getMirroring();
@@ -131,7 +130,8 @@
// Calculate sensorToBufferTransform
android.graphics.Matrix sensorToBufferTransform =
new android.graphics.Matrix(input.getSensorToBufferTransform());
- android.graphics.Matrix imageTransform = getRectToRect(sizeToRectF(inputSize),
+ android.graphics.Matrix imageTransform = getRectToRect(
+ sizeToRectF(input.getStreamSpec().getResolution()),
sizeToRectF(outConfig.getSize()), rotationDegrees, mirroring);
sensorToBufferTransform.postConcat(imageTransform);
@@ -140,9 +140,13 @@
Size rotatedCropSize = getRotatedSize(outConfig.getCropRect(), rotationDegrees);
checkArgument(isAspectRatioMatchingWithRoundingError(rotatedCropSize, outConfig.getSize()));
+ StreamSpec streamSpec = StreamSpec.builder(outConfig.getSize())
+ .setExpectedFrameRateRange(input.getStreamSpec().getExpectedFrameRateRange())
+ .build();
+
outputSurface = new SurfaceEdge(
outConfig.getTargets(),
- outConfig.getSize(),
+ streamSpec,
sensorToBufferTransform,
// The Surface transform cannot be carried over during buffer copy.
/*hasCameraTransform=*/false,
@@ -190,7 +194,7 @@
private void createAndSendSurfaceOutput(@NonNull SurfaceEdge input,
Map.Entry<OutConfig, SurfaceEdge> output) {
ListenableFuture<SurfaceOutput> future = output.getValue().createSurfaceOutputFuture(
- input.getSize(),
+ input.getStreamSpec().getResolution(),
output.getKey().getCropRect(),
input.getRotationDegrees(),
output.getKey().getMirroring());
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/ResolutionUtils.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/ResolutionUtils.java
new file mode 100644
index 0000000..7b93c0b
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/ResolutionUtils.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.camera.core.streamsharing;
+
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
+
+import android.os.Build;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.UseCaseConfig;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Utility methods for calculating resolutions.
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+class ResolutionUtils {
+
+ private ResolutionUtils() {
+ }
+
+ /**
+ * Returns a list of {@link Surface} resolution sorted by priority.
+ *
+ * <p> This method calculates the resolution for the parent {@link StreamSharing} based on 1)
+ * the supported PRIV resolutions, 2) the sensor size and 3) the children's configs.
+ */
+ static List<Size> getMergedResolutions(
+ @NonNull List<Size> supportedResolutions,
+ @NonNull Size sensorSize,
+ @NonNull Set<UseCaseConfig<?>> useCaseConfigs) {
+ // TODO(b/264936115): This is a temporary placeholder solution that returns the config of
+ // VideoCapture if it exists. Later we will update it to actually merge the children's
+ // configs.
+ for (UseCaseConfig<?> useCaseConfig : useCaseConfigs) {
+ List<Size> customOrderedResolutions =
+ useCaseConfig.retrieveOption(OPTION_CUSTOM_ORDERED_RESOLUTIONS, null);
+ if (customOrderedResolutions != null) {
+ return customOrderedResolutions;
+ }
+ }
+ return supportedResolutions;
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
index 738faba..f1a5619 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
@@ -25,6 +25,7 @@
import androidx.annotation.RequiresApi;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.ImageFormatConstants;
@@ -42,11 +43,11 @@
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class StreamSharing extends UseCase {
+ private static final StreamSharingConfig DEFAULT_CONFIG;
+
@SuppressWarnings("UnusedVariable")
private final VirtualCamera mVirtualCamera;
- private static final StreamSharingConfig DEFAULT_CONFIG;
-
static {
MutableConfig mutableConfig = new StreamSharingBuilder().getMutableConfig();
mutableConfig.insertOption(OPTION_INPUT_FORMAT,
@@ -93,6 +94,38 @@
}
@NonNull
+ @Override
+ protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+ @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
+ mVirtualCamera.mergeChildrenConfigs(builder.getMutableConfig());
+ return builder.getUseCaseConfig();
+ }
+
+ @Override
+ public void onBind() {
+ super.onBind();
+ mVirtualCamera.bindChildren();
+ }
+
+ @Override
+ public void onUnbind() {
+ super.onUnbind();
+ mVirtualCamera.unbindChildren();
+ }
+
+ @Override
+ public void onStateAttached() {
+ super.onStateAttached();
+ mVirtualCamera.notifyStateAttached();
+ }
+
+ @Override
+ public void onStateDetached() {
+ super.onStateDetached();
+ mVirtualCamera.notifyStateDetached();
+ }
+
+ @NonNull
public Set<UseCase> getChildren() {
return mVirtualCamera.getChildren();
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
index 0afef54..1a5ec39 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
@@ -16,7 +16,13 @@
package androidx.camera.core.streamsharing;
+import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
+import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
+import static androidx.camera.core.streamsharing.ResolutionUtils.getMergedResolutions;
+
import android.os.Build;
+import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
@@ -24,12 +30,17 @@
import androidx.camera.core.impl.CameraControlInternal;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.CameraInternal;
+import androidx.camera.core.impl.MutableConfig;
import androidx.camera.core.impl.Observable;
+import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
import com.google.common.util.concurrent.ListenableFuture;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
import java.util.Set;
/**
@@ -43,10 +54,8 @@
private static final String UNSUPPORTED_MESSAGE = "Operation not supported by VirtualCamera.";
- @SuppressWarnings("UnusedVariable")
@NonNull
private final Set<UseCase> mChildren;
- @SuppressWarnings("UnusedVariable")
@NonNull
private final UseCaseConfigFactory mUseCaseConfigFactory;
@NonNull
@@ -68,7 +77,47 @@
// --- API for StreamSharing ---
- // TODO(b/264936250): Add methods for interacting with the StreamSharing UseCase.
+ void mergeChildrenConfigs(@NonNull MutableConfig mutableConfig) {
+ Set<UseCaseConfig<?>> childrenConfigs = new HashSet<>();
+ for (UseCase useCase : mChildren) {
+ childrenConfigs.add(useCase.mergeConfigs(mParentCamera.getCameraInfoInternal(),
+ null,
+ useCase.getDefaultConfig(true, mUseCaseConfigFactory)));
+ }
+ // Merge resolution configs.
+ List<Size> supportedResolutions =
+ new ArrayList<>(mParentCamera.getCameraInfoInternal().getSupportedResolutions(
+ INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE));
+ Size sensorSize = rectToSize(mParentCamera.getCameraControlInternal().getSensorRect());
+ mutableConfig.insertOption(OPTION_CUSTOM_ORDERED_RESOLUTIONS,
+ getMergedResolutions(supportedResolutions, sensorSize,
+ childrenConfigs));
+ }
+
+ void bindChildren() {
+ for (UseCase useCase : mChildren) {
+ useCase.bindToCamera(this, null,
+ useCase.getDefaultConfig(true, mUseCaseConfigFactory));
+ }
+ }
+
+ void unbindChildren() {
+ for (UseCase useCase : mChildren) {
+ useCase.unbindFromCamera(this);
+ }
+ }
+
+ void notifyStateAttached() {
+ for (UseCase useCase : mChildren) {
+ useCase.onStateAttached();
+ }
+ }
+
+ void notifyStateDetached() {
+ for (UseCase useCase : mChildren) {
+ useCase.onStateDetached();
+ }
+ }
@NonNull
Set<UseCase> getChildren() {
@@ -110,6 +159,8 @@
@NonNull
@Override
public CameraInfoInternal getCameraInfoInternal() {
+ // TODO(264936250): replace this with a virtual camera info that returns a updated sensor
+ // rotation degrees based on buffer transformation applied in StreamSharing.
return mParentCamera.getCameraInfoInternal();
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 1d22a1f..d1dae4c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -32,6 +32,7 @@
import androidx.camera.core.impl.OptionsBundle
import androidx.camera.core.impl.PreviewConfig
import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.UseCaseConfig
import androidx.camera.core.impl.UseCaseConfigFactory
import androidx.camera.core.impl.utils.executor.CameraXExecutors
@@ -655,7 +656,8 @@
)
previewToDetach.bindToCamera(camera, null, previewConfig)
- previewToDetach.onSuggestedResolutionUpdated(Size(640, 480))
+ val streamSpec = StreamSpec.builder(Size(640, 480)).build()
+ previewToDetach.onSuggestedStreamSpecUpdated(streamSpec)
return previewToDetach
}
}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/StreamSpecTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/StreamSpecTest.kt
new file mode 100644
index 0000000..6142527
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/StreamSpecTest.kt
@@ -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 androidx.camera.core.impl
+
+import android.os.Build
+import android.util.Range
+import android.util.Size
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class StreamSpecTest {
+
+ @Test
+ fun canRetrieveResolution() {
+ val streamSpec = StreamSpec.builder(TEST_RESOLUTION).build()
+
+ assertThat(streamSpec.resolution).isEqualTo(TEST_RESOLUTION)
+ }
+
+ @Test
+ fun defaultExpectedFrameRateRangeIsUnspecified() {
+ val streamSpec = StreamSpec.builder(TEST_RESOLUTION).build()
+
+ assertThat(streamSpec.expectedFrameRateRange)
+ .isEqualTo(StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED)
+ }
+
+ @Test
+ fun canRetrieveExpectedFrameRateRange() {
+ val streamSpec = StreamSpec.builder(TEST_RESOLUTION)
+ .setExpectedFrameRateRange(TEST_EXPECTED_FRAME_RATE_RANGE)
+ .build()
+
+ assertThat(streamSpec.expectedFrameRateRange).isEqualTo(TEST_EXPECTED_FRAME_RATE_RANGE)
+ }
+
+ companion object {
+ private val TEST_RESOLUTION = Size(640, 480)
+ private val TEST_EXPECTED_FRAME_RATE_RANGE = Range(30, 30)
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
index 26ff45d..6680e50 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
@@ -36,6 +36,7 @@
import androidx.camera.core.impl.ImageFormatConstants
import androidx.camera.core.impl.MutableOptionsBundle
import androidx.camera.core.impl.OptionsBundle
+import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.UseCaseConfigFactory
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
import androidx.camera.core.internal.CameraUseCaseAdapter.CameraException
@@ -137,6 +138,9 @@
StreamSharing::class.java,
ImageCapture::class.java
)
+ // Assert: StreamSharing children are bound
+ assertThat(preview.camera).isNotNull()
+ assertThat(video.camera).isNotNull()
}
@Test
@@ -179,6 +183,10 @@
StreamSharing::class.java,
ImageCapture::class.java
)
+ // Assert: StreamSharing children are bound
+ assertThat(preview.camera).isNotNull()
+ assertThat(video.camera).isNotNull()
+ assertThat(image.camera).isNotNull()
}
@Test
@@ -187,18 +195,22 @@
adapter.setStreamSharingEnabled(true)
// Act: add UseCases that need StreamSharing.
adapter.addUseCases(setOf(preview, video, image))
- // Assert: StreamSharing exists
+ // Assert: StreamSharing exists and bound.
adapter.cameraUseCases.hasExactTypes(
StreamSharing::class.java,
ImageCapture::class.java
)
+ val streamSharing =
+ adapter.cameraUseCases.filterIsInstance(StreamSharing::class.java).single()
+ assertThat(streamSharing.camera).isNotNull()
// Act: remove UseCase so that StreamSharing is no longer needed
adapter.removeUseCases(setOf(video))
- // Assert: StreamSharing removed.
+ // Assert: StreamSharing removed and unbound.
adapter.cameraUseCases.hasExactTypes(
Preview::class.java,
ImageCapture::class.java
)
+ assertThat(streamSharing.camera).isNull()
}
@Test(expected = CameraException::class)
@@ -474,10 +486,10 @@
// Arrange: set up adapter with aspect ratio 1.
// The sensor size is 4032x3024 defined in FakeCameraDeviceSurfaceManager
- fakeCameraDeviceSurfaceManager.setSuggestedResolution(
+ fakeCameraDeviceSurfaceManager.setSuggestedStreamSpec(
CAMERA_ID,
FakeUseCaseConfig::class.java,
- Size(4032, 3022)
+ StreamSpec.builder(Size(4032, 3022)).build()
)
/* Sensor to Buffer Crop on Buffer
* 0 4032
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEdgeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEdgeTest.kt
index 4fa3664..60e066b 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEdgeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEdgeTest.kt
@@ -22,9 +22,11 @@
import android.graphics.SurfaceTexture
import android.os.Build
import android.os.Looper.getMainLooper
+import android.util.Range
import android.util.Size
import android.view.Surface
import androidx.camera.core.CameraEffect
+import androidx.camera.core.CameraEffect.PREVIEW
import androidx.camera.core.SurfaceOutput
import androidx.camera.core.SurfaceRequest
import androidx.camera.core.SurfaceRequest.Result.RESULT_REQUEST_CANCELLED
@@ -32,6 +34,7 @@
import androidx.camera.core.impl.DeferrableSurface
import androidx.camera.core.impl.DeferrableSurface.SurfaceClosedException
import androidx.camera.core.impl.DeferrableSurface.SurfaceUnavailableException
+import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.utils.TransformUtils.sizeToRect
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
import androidx.camera.core.impl.utils.futures.FutureCallback
@@ -60,6 +63,9 @@
companion object {
private val INPUT_SIZE = Size(640, 480)
+ private val FRAME_RATE = Range.create(30, 30)
+ private val FRAME_SPEC =
+ StreamSpec.builder(INPUT_SIZE).setExpectedFrameRateRange(FRAME_RATE).build()
}
private lateinit var surfaceEdge: SurfaceEdge
@@ -70,7 +76,7 @@
@Before
fun setUp() {
surfaceEdge = SurfaceEdge(
- CameraEffect.PREVIEW, INPUT_SIZE,
+ CameraEffect.PREVIEW, StreamSpec.builder(INPUT_SIZE).build(),
Matrix(), true, Rect(), 0, false
)
fakeSurfaceTexture = SurfaceTexture(0)
@@ -86,6 +92,14 @@
fakeSurface.release()
}
+ @Test
+ fun createWithStreamSpec_canGetStreamSpec() {
+ val edge = SurfaceEdge(
+ PREVIEW, FRAME_SPEC, Matrix(), true, Rect(), 0, false
+ )
+ assertThat(edge.streamSpec).isEqualTo(FRAME_SPEC)
+ }
+
@Test(expected = IllegalStateException::class)
fun setProviderOnClosedEdge_throwsException() {
surfaceEdge.close()
@@ -253,7 +267,13 @@
private fun getSurfaceRequestHasTransform(hasCameraTransform: Boolean): Boolean {
// Arrange.
val surface = SurfaceEdge(
- CameraEffect.PREVIEW, Size(640, 480), Matrix(), hasCameraTransform, Rect(), 0, false
+ CameraEffect.PREVIEW,
+ StreamSpec.builder(Size(640, 480)).build(),
+ Matrix(),
+ hasCameraTransform,
+ Rect(),
+ 0,
+ false
)
var transformationInfo: TransformationInfo? = null
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
index 745ce5b..5628d5f 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
@@ -20,12 +20,14 @@
import android.graphics.SurfaceTexture
import android.os.Build
import android.os.Looper.getMainLooper
+import android.util.Range
import android.util.Size
import android.view.Surface
import androidx.camera.core.CameraEffect.PREVIEW
import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
import androidx.camera.core.SurfaceRequest
import androidx.camera.core.SurfaceRequest.TransformationInfo
+import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.utils.TransformUtils.is90or270
import androidx.camera.core.impl.utils.TransformUtils.rectToSize
import androidx.camera.core.impl.utils.TransformUtils.rotateSize
@@ -141,12 +143,12 @@
// Assert: with transformation, the output size is cropped/rotated and the rotation
// degrees is reset.
val previewOutput = nodeOutput[previewOutConfig]!!
- assertThat(previewOutput.size).isEqualTo(rectToSize(expectedCropRect))
+ assertThat(previewOutput.streamSpec.resolution).isEqualTo(rectToSize(expectedCropRect))
assertThat(previewOutput.cropRect).isEqualTo(expectedCropRect)
assertThat(previewOutput.rotationDegrees).isEqualTo(0)
assertThat(previewOutput.mirroring).isFalse()
val videoOutput = nodeOutput[videoOutConfig]!!
- assertThat(videoOutput.size).isEqualTo(videoOutputSize)
+ assertThat(videoOutput.streamSpec.resolution).isEqualTo(videoOutputSize)
assertThat(videoOutput.cropRect).isEqualTo(sizeToRect(videoOutputSize))
assertThat(videoOutput.rotationDegrees).isEqualTo(0)
assertThat(videoOutput.mirroring).isTrue()
@@ -168,6 +170,23 @@
}
@Test
+ fun transformInputWithFrameRate_propagatesToChildren() {
+ // Arrange: create input edge with frame rate.
+ val frameRateRange = Range.create(30, 30)
+ createSurfaceProcessorNode()
+ createInputEdge(
+ frameRateRange = frameRateRange
+ )
+ // Act.
+ val nodeOutput = node.transform(nodeInput)
+ // Assert: all outputs have the same frame rate.
+ assertThat(nodeOutput[previewOutConfig]!!.streamSpec.expectedFrameRateRange)
+ .isEqualTo(frameRateRange)
+ assertThat(nodeOutput[videoOutConfig]!!.streamSpec.expectedFrameRateRange)
+ .isEqualTo(frameRateRange)
+ }
+
+ @Test
fun transformInput_applyCropRotateAndMirroring_initialTransformInfoIsPropagated() {
// Arrange.
createSurfaceProcessorNode()
@@ -284,16 +303,17 @@
previewCropRect: Rect = PREVIEW_CROP_RECT,
previewRotationDegrees: Int = ROTATION_DEGREES,
mirroring: Boolean = MIRRORING,
- videoOutputSize: Size = VIDEO_SIZE
+ videoOutputSize: Size = VIDEO_SIZE,
+ frameRateRange: Range<Int> = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED
) {
- val surface = SurfaceEdge(
+ val inputEdge = SurfaceEdge(
previewTarget,
- previewSize,
+ StreamSpec.builder(previewSize).setExpectedFrameRateRange(frameRateRange).build(),
sensorToBufferTransform,
hasCameraTransform,
previewCropRect,
previewRotationDegrees,
- mirroring
+ mirroring,
)
videoOutConfig = OutConfig.of(
VIDEO_CAPTURE,
@@ -301,9 +321,9 @@
videoOutputSize,
true
)
- previewOutConfig = OutConfig.of(surface)
+ previewOutConfig = OutConfig.of(inputEdge)
nodeInput = SurfaceProcessorNode.In.of(
- surface,
+ inputEdge,
listOf(previewOutConfig, videoOutConfig)
)
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
index 3f61631..46c8367 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
@@ -17,7 +17,6 @@
package androidx.camera.core.streamsharing
import android.os.Build
-import androidx.camera.core.Preview
import androidx.camera.core.impl.UseCaseConfigFactory
import androidx.camera.core.internal.TargetConfig.OPTION_TARGET_CLASS
import androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME
@@ -41,14 +40,51 @@
class StreamSharingTest {
private val parentCamera = FakeCamera()
- private val preview = Preview.Builder().build()
- private val video = FakeUseCase()
+ private val child1 = FakeUseCase()
+ private val child2 = FakeUseCase()
private val useCaseConfigFactory = FakeUseCaseConfigFactory()
+ private val camera = FakeCamera()
private lateinit var streamSharing: StreamSharing
@Before
fun setUp() {
- streamSharing = StreamSharing(parentCamera, setOf(preview, video), useCaseConfigFactory)
+ streamSharing = StreamSharing(parentCamera, setOf(child1, child2), useCaseConfigFactory)
+ }
+
+ @Test
+ fun bindAndUnbindParent_propagatesToChildren() {
+ // Assert: children not bound to camera by default.
+ assertThat(child1.camera).isNull()
+ assertThat(child2.camera).isNull()
+ // Act: bind to camera.
+ streamSharing.bindToCamera(camera, null, null)
+ // Assert: children bound to the virtual camera.
+ assertThat(child1.camera).isInstanceOf(VirtualCamera::class.java)
+ assertThat(child1.mergedConfigRetrieved).isTrue()
+ assertThat(child2.camera).isInstanceOf(VirtualCamera::class.java)
+ assertThat(child2.mergedConfigRetrieved).isTrue()
+ // Act: unbind.
+ streamSharing.unbindFromCamera(camera)
+ // Assert: children not bound.
+ assertThat(child1.camera).isNull()
+ assertThat(child2.camera).isNull()
+ }
+
+ @Test
+ fun attachAndDetachParent_propagatesToChildren() {
+ // Assert: children not attached by default.
+ assertThat(child1.stateAttachedCount).isEqualTo(0)
+ assertThat(child2.stateAttachedCount).isEqualTo(0)
+ // Act: attach.
+ streamSharing.onStateAttached()
+ // Assert: children attached.
+ assertThat(child1.stateAttachedCount).isEqualTo(1)
+ assertThat(child2.stateAttachedCount).isEqualTo(1)
+ // Act: detach.
+ streamSharing.onStateDetached()
+ // Assert: children not attached.
+ assertThat(child1.stateAttachedCount).isEqualTo(0)
+ assertThat(child2.stateAttachedCount).isEqualTo(0)
}
@Test
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
index 42ecd87..e44b6e7 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
@@ -30,6 +30,7 @@
import androidx.annotation.RequiresApi;
import androidx.camera.core.impl.AttachedSurfaceInfo;
import androidx.camera.core.impl.CameraDeviceSurfaceManager;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.SurfaceConfig;
import androidx.camera.core.impl.UseCaseConfig;
@@ -46,25 +47,25 @@
public static final Size MAX_OUTPUT_SIZE = new Size(4032, 3024); // 12.2 MP
- private final Map<String, Map<Class<? extends UseCaseConfig<?>>, Size>> mDefinedResolutions =
- new HashMap<>();
+ private final Map<String, Map<Class<? extends UseCaseConfig<?>>, StreamSpec>>
+ mDefinedStreamSpecs = new HashMap<>();
private Set<List<Integer>> mValidSurfaceCombos = createDefaultValidSurfaceCombos();
/**
- * Sets the given suggested resolutions for the specified camera Id and use case type.
+ * Sets the given suggested stream specs for the specified camera Id and use case type.
*/
- public void setSuggestedResolution(@NonNull String cameraId,
+ public void setSuggestedStreamSpec(@NonNull String cameraId,
@NonNull Class<? extends UseCaseConfig<?>> type,
- @NonNull Size size) {
- Map<Class<? extends UseCaseConfig<?>>, Size> useCaseConfigTypeToSizeMap =
- mDefinedResolutions.get(cameraId);
- if (useCaseConfigTypeToSizeMap == null) {
- useCaseConfigTypeToSizeMap = new HashMap<>();
- mDefinedResolutions.put(cameraId, useCaseConfigTypeToSizeMap);
+ @NonNull StreamSpec streamSpec) {
+ Map<Class<? extends UseCaseConfig<?>>, StreamSpec> useCaseConfigTypeToStreamSpecMap =
+ mDefinedStreamSpecs.get(cameraId);
+ if (useCaseConfigTypeToStreamSpecMap == null) {
+ useCaseConfigTypeToStreamSpecMap = new HashMap<>();
+ mDefinedStreamSpecs.put(cameraId, useCaseConfigTypeToStreamSpecMap);
}
- useCaseConfigTypeToSizeMap.put(type, size);
+ useCaseConfigTypeToStreamSpecMap.put(type, streamSpec);
}
@Override
@@ -85,27 +86,27 @@
@Override
@NonNull
- public Map<UseCaseConfig<?>, Size> getSuggestedResolutions(
+ public Map<UseCaseConfig<?>, StreamSpec> getSuggestedStreamSpecs(
@NonNull String cameraId,
@NonNull List<AttachedSurfaceInfo> existingSurfaces,
@NonNull List<UseCaseConfig<?>> newUseCaseConfigs) {
checkSurfaceCombo(existingSurfaces, newUseCaseConfigs);
- Map<UseCaseConfig<?>, Size> suggestedSizes = new HashMap<>();
+ Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecs = new HashMap<>();
for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
- Size resolution = MAX_OUTPUT_SIZE;
- Map<Class<? extends UseCaseConfig<?>>, Size> definedResolutions =
- mDefinedResolutions.get(cameraId);
- if (definedResolutions != null) {
- Size definedResolution = definedResolutions.get(useCaseConfig.getClass());
- if (definedResolution != null) {
- resolution = definedResolution;
+ StreamSpec streamSpec = StreamSpec.builder(MAX_OUTPUT_SIZE).build();
+ Map<Class<? extends UseCaseConfig<?>>, StreamSpec> definedStreamSpecs =
+ mDefinedStreamSpecs.get(cameraId);
+ if (definedStreamSpecs != null) {
+ StreamSpec definedStreamSpec = definedStreamSpecs.get(useCaseConfig.getClass());
+ if (definedStreamSpec != null) {
+ streamSpec = definedStreamSpec;
}
}
- suggestedSizes.put(useCaseConfig, resolution);
+ suggestedStreamSpecs.put(useCaseConfig, streamSpec);
}
- return suggestedSizes;
+ return suggestedStreamSpecs;
}
/**
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java
index 4b87e5b..e102bc6 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java
@@ -16,15 +16,15 @@
package androidx.camera.testing.fakes;
-import android.util.Size;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType;
@@ -39,6 +39,7 @@
private volatile boolean mIsDetached = false;
private final AtomicInteger mStateAttachedCount = new AtomicInteger(0);
private final CaptureType mCaptureType;
+ private boolean mMergedConfigRetrieved = false;
/**
* Creates a new instance of a {@link FakeUseCase} with a given configuration and capture type.
@@ -72,7 +73,8 @@
@Override
public UseCaseConfig.Builder<?, ?, ?> getUseCaseConfigBuilder(@NonNull Config config) {
return new FakeUseCaseConfig.Builder(config)
- .setSessionOptionUnpacker((useCaseConfig, sessionConfigBuilder) -> { });
+ .setSessionOptionUnpacker((useCaseConfig, sessionConfigBuilder) -> {
+ });
}
/**
@@ -91,6 +93,14 @@
return config == null ? null : getUseCaseConfigBuilder(config).getUseCaseConfig();
}
+ @NonNull
+ @Override
+ protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+ @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
+ mMergedConfigRetrieved = true;
+ return builder.getUseCaseConfig();
+ }
+
@Override
public void onUnbind() {
super.onUnbind();
@@ -104,9 +114,15 @@
}
@Override
+ public void onStateDetached() {
+ super.onStateDetached();
+ mStateAttachedCount.decrementAndGet();
+ }
+
+ @Override
@NonNull
- protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
- return suggestedResolution;
+ protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
+ return suggestedStreamSpec;
}
/**
@@ -122,4 +138,11 @@
public int getStateAttachedCount() {
return mStateAttachedCount.get();
}
+
+ /**
+ * Returns true if {@link #mergeConfigs} have been invoked.
+ */
+ public boolean getMergedConfigRetrieved() {
+ return mMergedConfigRetrieved;
+ }
}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
index c6b7329..794cd97 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
@@ -33,6 +33,7 @@
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.impl.AttachedSurfaceInfo;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.SurfaceConfig;
import androidx.camera.core.impl.UseCaseConfig;
@@ -75,10 +76,12 @@
mFakeCameraDeviceSurfaceManager = new FakeCameraDeviceSurfaceManager();
mFakeUseCaseConfig = new FakeUseCaseConfig.Builder().getUseCaseConfig();
- mFakeCameraDeviceSurfaceManager.setSuggestedResolution(FAKE_CAMERA_ID0,
- mFakeUseCaseConfig.getClass(), new Size(FAKE_WIDTH0, FAKE_HEIGHT0));
- mFakeCameraDeviceSurfaceManager.setSuggestedResolution(FAKE_CAMERA_ID1,
- mFakeUseCaseConfig.getClass(), new Size(FAKE_WIDTH1, FAKE_HEIGHT1));
+ mFakeCameraDeviceSurfaceManager.setSuggestedStreamSpec(FAKE_CAMERA_ID0,
+ mFakeUseCaseConfig.getClass(),
+ StreamSpec.builder(new Size(FAKE_WIDTH0, FAKE_HEIGHT0)).build());
+ mFakeCameraDeviceSurfaceManager.setSuggestedStreamSpec(FAKE_CAMERA_ID1,
+ mFakeUseCaseConfig.getClass(),
+ StreamSpec.builder(new Size(FAKE_WIDTH1, FAKE_HEIGHT1)).build());
mUseCaseConfigList = singletonList(mFakeUseCaseConfig);
}
@@ -87,7 +90,7 @@
public void validSurfaceCombination_noException() {
UseCaseConfig<?> preview = new FakeUseCaseConfig.Builder().getUseCaseConfig();
UseCaseConfig<?> analysis = new ImageAnalysis.Builder().getUseCaseConfig();
- mFakeCameraDeviceSurfaceManager.getSuggestedResolutions(FAKE_CAMERA_ID0,
+ mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(FAKE_CAMERA_ID0,
emptyList(), asList(preview, analysis));
}
@@ -100,7 +103,7 @@
YUV_420_888,
new Size(1, 1),
new Range<>(30, 30));
- mFakeCameraDeviceSurfaceManager.getSuggestedResolutions(FAKE_CAMERA_ID0,
+ mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(FAKE_CAMERA_ID0,
singletonList(analysis), asList(preview, video));
}
@@ -109,23 +112,23 @@
UseCaseConfig<?> preview = new FakeUseCaseConfig.Builder().getUseCaseConfig();
UseCaseConfig<?> video = new FakeUseCaseConfig.Builder().getUseCaseConfig();
UseCaseConfig<?> analysis = new ImageAnalysis.Builder().getUseCaseConfig();
- mFakeCameraDeviceSurfaceManager.getSuggestedResolutions(FAKE_CAMERA_ID0,
+ mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(FAKE_CAMERA_ID0,
Collections.emptyList(), asList(preview, video, analysis));
}
@Test
- public void canRetrieveInsertedSuggestedResolutions() {
- Map<UseCaseConfig<?>, Size> suggestedSizesCamera0 =
- mFakeCameraDeviceSurfaceManager.getSuggestedResolutions(FAKE_CAMERA_ID0,
+ public void canRetrieveInsertedSuggestedStreamSpecs() {
+ Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecsCamera0 =
+ mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(FAKE_CAMERA_ID0,
Collections.emptyList(), mUseCaseConfigList);
- Map<UseCaseConfig<?>, Size> suggestedSizesCamera1 =
- mFakeCameraDeviceSurfaceManager.getSuggestedResolutions(FAKE_CAMERA_ID1,
+ Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecCamera1 =
+ mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(FAKE_CAMERA_ID1,
Collections.emptyList(), mUseCaseConfigList);
- assertThat(suggestedSizesCamera0.get(mFakeUseCaseConfig)).isEqualTo(
- new Size(FAKE_WIDTH0, FAKE_HEIGHT0));
- assertThat(suggestedSizesCamera1.get(mFakeUseCaseConfig)).isEqualTo(
- new Size(FAKE_WIDTH1, FAKE_HEIGHT1));
+ assertThat(suggestedStreamSpecsCamera0.get(mFakeUseCaseConfig)).isEqualTo(
+ StreamSpec.builder(new Size(FAKE_WIDTH0, FAKE_HEIGHT0)).build());
+ assertThat(suggestedStreamSpecCamera1.get(mFakeUseCaseConfig)).isEqualTo(
+ StreamSpec.builder(new Size(FAKE_WIDTH1, FAKE_HEIGHT1)).build());
}
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 478ee69..7019ae6 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -90,6 +90,7 @@
import androidx.camera.core.impl.Observable.Observer;
import androidx.camera.core.impl.OptionsBundle;
import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.Timebase;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
@@ -280,14 +281,14 @@
@Override
public void onStateAttached() {
super.onStateAttached();
- Preconditions.checkNotNull(getAttachedSurfaceResolution(), "The suggested resolution "
- + "should be already updated and shouldn't be null.");
+ Preconditions.checkNotNull(getAttachedStreamSpec(), "The suggested stream "
+ + "specification should be already updated and shouldn't be null.");
Preconditions.checkState(mSurfaceRequest == null, "The surface request should be null "
+ "when VideoCapture is attached.");
mStreamInfo = fetchObservableValue(getOutput().getStreamInfo(),
StreamInfo.STREAM_INFO_ANY_INACTIVE);
mSessionConfigBuilder = createPipeline(getCameraId(),
- (VideoCaptureConfig<T>) getCurrentConfig(), getAttachedSurfaceResolution());
+ (VideoCaptureConfig<T>) getCurrentConfig(), getAttachedStreamSpec());
applyStreamInfoToSessionConfigBuilder(mSessionConfigBuilder, mStreamInfo);
updateSessionConfig(mSessionConfigBuilder.build());
// VideoCapture has to be active to apply SessionConfig's template type.
@@ -452,9 +453,10 @@
@NonNull
private SessionConfig.Builder createPipeline(@NonNull String cameraId,
@NonNull VideoCaptureConfig<T> config,
- @NonNull Size resolution) {
+ @NonNull StreamSpec streamSpec) {
Threads.checkMainThread();
CameraInternal camera = Preconditions.checkNotNull(getCamera());
+ Size resolution = streamSpec.getResolution();
// Currently, VideoCapture uses StreamInfo to handle requests for surface, so
// handleInvalidate() is not used. But if a different approach is asked in the future,
@@ -479,7 +481,7 @@
timebase = camera.getCameraInfoInternal().getTimebase();
SurfaceEdge cameraEdge = new SurfaceEdge(
VIDEO_CAPTURE,
- resolution,
+ streamSpec,
getSensorToBufferTransformMatrix(),
getHasCameraTransform(),
mCropRect,
@@ -526,7 +528,7 @@
SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
sessionConfigBuilder.addErrorListener(
- (sessionConfig, error) -> resetPipeline(cameraId, config, resolution));
+ (sessionConfig, error) -> resetPipeline(cameraId, config, streamSpec));
if (USE_TEMPLATE_PREVIEW_BY_QUIRK) {
sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
}
@@ -572,7 +574,7 @@
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
void resetPipeline(@NonNull String cameraId,
@NonNull VideoCaptureConfig<T> config,
- @NonNull Size resolution) {
+ @NonNull StreamSpec streamSpec) {
clearPipeline();
// Ensure the attached camera has not changed before resetting.
@@ -580,7 +582,7 @@
// to this use case so we don't need to do this check.
if (isCurrentCamera(cameraId)) {
// Only reset the pipeline when the bound camera is the same.
- mSessionConfigBuilder = createPipeline(cameraId, config, resolution);
+ mSessionConfigBuilder = createPipeline(cameraId, config, streamSpec);
applyStreamInfoToSessionConfigBuilder(mSessionConfigBuilder, mStreamInfo);
updateSessionConfig(mSessionConfigBuilder.build());
notifyReset();
@@ -669,7 +671,7 @@
// Reset pipeline if the stream ids are different, which means there's a new
// surface ready to be requested.
resetPipeline(getCameraId(), (VideoCaptureConfig<T>) getCurrentConfig(),
- Preconditions.checkNotNull(getAttachedSurfaceResolution()));
+ Preconditions.checkNotNull(getAttachedStreamSpec()));
} else if ((currentStreamInfo.getId() != STREAM_ID_ERROR
&& streamInfo.getId() == STREAM_ID_ERROR)
|| (currentStreamInfo.getId() == STREAM_ID_ERROR
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index fdfd577..945dd35 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -39,6 +39,7 @@
import androidx.camera.core.impl.ImageOutputConfig
import androidx.camera.core.impl.MutableStateObservable
import androidx.camera.core.impl.Observable
+import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.Timebase
import androidx.camera.core.impl.utils.CompareSizesByArea
import androidx.camera.core.impl.utils.TransformUtils.rectToSize
@@ -300,7 +301,7 @@
Surface.ROTATION_270
).forEach { targetRotation ->
// Arrange.
- setSuggestedResolution(quality)
+ setSuggestedStreamSpec(quality)
var surfaceRequest: SurfaceRequest? = null
val videoOutput = createVideoOutput(
mediaSpec = MediaSpec.builder().configureVideo {
@@ -404,7 +405,7 @@
// Camera 0 support 2160P(UHD) and 720P(HD)
arrayOf(UHD, HD, HIGHEST, LOWEST).forEach { quality ->
- setSuggestedResolution(quality)
+ setSuggestedStreamSpec(quality)
val videoOutput = createVideoOutput(
mediaSpec = MediaSpec.builder().configureVideo {
@@ -439,7 +440,7 @@
)
)
createCameraUseCaseAdapter()
- setSuggestedResolution(RESOLUTION_480P)
+ setSuggestedStreamSpec(StreamSpec.builder(RESOLUTION_480P).build())
val videoOutput = createVideoOutput(
mediaSpec = MediaSpec.builder().configureVideo {
@@ -552,7 +553,7 @@
// Arrange.
setupCamera()
createCameraUseCaseAdapter()
- setSuggestedResolution(Size(639, 479))
+ setSuggestedStreamSpec(StreamSpec.builder(Size(639, 479)).build())
val videoOutput = createVideoOutput()
val videoCapture = createVideoCapture(
@@ -884,7 +885,7 @@
// Arrange.
setupCamera()
createCameraUseCaseAdapter()
- setSuggestedResolution(quality)
+ setSuggestedStreamSpec(quality)
var surfaceRequest: SurfaceRequest? = null
val videoOutput = createVideoOutput(
mediaSpec = MediaSpec.builder().configureVideo {
@@ -1009,15 +1010,15 @@
private fun createFakeSurfaceProcessor() = FakeSurfaceProcessorInternal(mainThreadExecutor())
- private fun setSuggestedResolution(quality: Quality) {
- setSuggestedResolution(CAMERA_0_QUALITY_SIZE[quality]!!)
+ private fun setSuggestedStreamSpec(quality: Quality) {
+ setSuggestedStreamSpec(StreamSpec.builder(CAMERA_0_QUALITY_SIZE[quality]!!).build())
}
- private fun setSuggestedResolution(resolution: Size) {
- surfaceManager.setSuggestedResolution(
+ private fun setSuggestedStreamSpec(streamSpec: StreamSpec) {
+ surfaceManager.setSuggestedStreamSpec(
CAMERA_ID_0,
VideoCaptureConfig::class.java,
- resolution
+ streamSpec
)
}
diff --git a/camera/integration-tests/avsynctestapp/build.gradle b/camera/integration-tests/avsynctestapp/build.gradle
index 8612bb7..0f4683f 100644
--- a/camera/integration-tests/avsynctestapp/build.gradle
+++ b/camera/integration-tests/avsynctestapp/build.gradle
@@ -53,6 +53,7 @@
implementation("androidx.compose.ui:ui:$compose_version")
implementation("androidx.compose.ui:ui-tooling-preview:$compose_version")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1")
+ implementation(project(":lifecycle:lifecycle-viewmodel"))
compileOnly(libs.kotlinCompiler)
@@ -61,6 +62,7 @@
testImplementation(libs.junit)
testImplementation(libs.truth)
androidTestImplementation(project(":camera:camera-testing"))
+ androidTestImplementation(project(":lifecycle:lifecycle-viewmodel"))
androidTestImplementation(libs.kotlinCoroutinesTest)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRules)
diff --git a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt
index 36b13db..88c8b26 100644
--- a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt
+++ b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt
@@ -66,12 +66,12 @@
private lateinit var viewModel: SignalGeneratorViewModel
private lateinit var lifecycleOwner: FakeLifecycleOwner
private val fakeViewModelStoreOwner = object : ViewModelStoreOwner {
- private val viewModelStore = ViewModelStore()
+ private val vmStore = ViewModelStore()
- override fun getViewModelStore() = viewModelStore
+ override val viewModelStore = vmStore
fun clear() {
- viewModelStore.clear()
+ vmStore.clear()
}
}
diff --git a/collection/collection-benchmark/build.gradle b/collection/collection-benchmark/build.gradle
index 9d23ccc..dc0cf11 100644
--- a/collection/collection-benchmark/build.gradle
+++ b/collection/collection-benchmark/build.gradle
@@ -118,6 +118,7 @@
scheme = "testapp-ios"
// ios 13, 15.2
destination = "platform=iOS Simulator,name=iPhone 13,OS=15.2"
+ referenceSha.set(androidx.getReferenceSha())
}
android {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
index 8fb61ca..4a60e10 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
@@ -1688,4 +1688,64 @@
fun Text(value: String) { }
"""
)
+
+ @Test
+ fun testVarargsIntrinsicRemember() = verifyComposeIrTransform(
+ source = """
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test(vararg strings: String) {
+ val show = remember { mutableStateOf(false) }
+ if (show.value) {
+ Text("Showing")
+ }
+ }
+ """,
+ extra = """
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Text(value: String) { }
+ """,
+ expectedTransformed = """
+ @Composable
+ fun Test(strings: Array<out String>, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test)<rememb...>,<Text("...>:Test.kt")
+ val %dirty = %changed
+ %composer.startMovableGroup(<>, strings.size)
+ val tmp0_iterator = strings.iterator()
+ while (tmp0_iterator.hasNext()) {
+ val value = tmp0_iterator.next()
+ %dirty = %dirty or if (%composer.changed(value)) 0b0100 else 0
+ }
+ %composer.endMovableGroup()
+ if (%dirty and 0b1110 === 0) {
+ %dirty = %dirty or 0b0010
+ }
+ if (%dirty and 0b0001 !== 0 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ val show = remember({
+ mutableStateOf(
+ value = false
+ )
+ }, %composer, 0)
+ if (show.value) {
+ Text("Showing", %composer, 0b0110)
+ }
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(*strings, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """
+ )
}
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 2465964..0b8e35a 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
@@ -117,7 +117,7 @@
* The maven version string of this compiler. This string should be updated before/after every
* release.
*/
- const val compilerVersion: String = "1.4.0"
+ const val compilerVersion: String = "1.4.1"
private val minimumRuntimeVersion: String
get() = runtimeVersionToMavenVersionTable[minimumRuntimeVersionInt] ?: "unknown"
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index 6cd17ef..5ec690d 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -860,7 +860,10 @@
val emitTraceMarkers = traceEventMarkersEnabled && !scope.function.isInline
- scope.updateIntrinsiceRememberSafety(!mightUseDefaultGroup(false, scope, defaultParam))
+ scope.updateIntrinsiceRememberSafety(
+ !mightUseDefaultGroup(false, scope, defaultParam) &&
+ !mightUseVarArgsGroup(false, scope)
+ )
transformed = transformed.apply {
transformChildrenVoid()
@@ -1001,7 +1004,10 @@
val emitTraceMarkers = traceEventMarkersEnabled && !scope.isInlinedLambda
- scope.updateIntrinsiceRememberSafety(!mightUseDefaultGroup(canSkipExecution, scope, null))
+ scope.updateIntrinsiceRememberSafety(
+ !mightUseDefaultGroup(canSkipExecution, scope, null) &&
+ !mightUseVarArgsGroup(canSkipExecution, scope)
+ )
// we must transform the body first, since that will allow us to see whether or not we
// are using the dispatchReceiverParameter or the extensionReceiverParameter
@@ -1157,7 +1163,8 @@
val defaultScope = transformDefaults(scope)
scope.updateIntrinsiceRememberSafety(
- !mightUseDefaultGroup(true, scope, defaultParam)
+ !mightUseDefaultGroup(true, scope, defaultParam) &&
+ !mightUseVarArgsGroup(true, scope)
)
// we must transform the body first, since that will allow us to see whether or not we
@@ -1335,6 +1342,13 @@
return parameters.any { it.defaultValue?.expression?.isStatic() == false }
}
+ // Like mightUseDefaultGroup(), this is an intentionally conservative value that must be true
+ // when ever a varargs group could be generated but can be true when it is not.
+ private fun mightUseVarArgsGroup(
+ isSkippableDeclaration: Boolean,
+ scope: Scope.FunctionScope
+ ) = isSkippableDeclaration && scope.allTrackedParams.any { it.isVararg }
+
private fun buildPreambleStatementsAndReturnIfSkippingPossible(
sourceElement: IrElement,
skipPreamble: IrStatementContainer,
@@ -1535,6 +1549,7 @@
irGet(param),
param.type.classOrNull!!.getPropertyGetter("size")!!.owner
)
+
// TODO(lmr): verify this works with default vararg expressions!
skipPreamble.statements.add(
irStartMovableGroup(
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
index 2c7a825..2a392bb 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
@@ -50,6 +50,7 @@
"Foundation",
listOf(
ComposableDemo("Draggable, Scrollable, Zoomable, Focusable") { HighLevelGesturesDemo() },
+ ComposableDemo("Overscroll") { OverscrollDemo() },
ComposableDemo("Can scroll forward / backward") { CanScrollSample() },
ComposableDemo("Vertical scroll") { VerticalScrollExample() },
ComposableDemo("Controlled Scrollable Row") { ControlledScrollableRowSample() },
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/HighLevelGesturesDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/HighLevelGesturesDemo.kt
index c0bab6f..d53f3d1 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/HighLevelGesturesDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/HighLevelGesturesDemo.kt
@@ -17,15 +17,12 @@
package androidx.compose.foundation.demos
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.samples.DraggableSample
import androidx.compose.foundation.samples.FocusableSample
import androidx.compose.foundation.samples.HoverableSample
-import androidx.compose.foundation.samples.OverscrollSample
import androidx.compose.foundation.samples.ScrollableSample
import androidx.compose.foundation.samples.TransformableSample
import androidx.compose.foundation.verticalScroll
@@ -40,11 +37,7 @@
Column(Modifier.verticalScroll(rememberScrollState())) {
DraggableSample()
Spacer(Modifier.height(50.dp))
- Row {
- ScrollableSample()
- Spacer(Modifier.width(30.dp))
- OverscrollSample()
- }
+ ScrollableSample()
Spacer(Modifier.height(50.dp))
TransformableSample()
Spacer(Modifier.height(50.dp))
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/OverscrollDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/OverscrollDemo.kt
new file mode 100644
index 0000000..df7592a
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/OverscrollDemo.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.compose.foundation.demos
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.samples.OverscrollWithDraggable_After
+import androidx.compose.foundation.samples.OverscrollWithDraggable_Before
+import androidx.compose.foundation.samples.OverscrollSample
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Preview
+@Composable
+fun OverscrollDemo() {
+ Column(
+ Modifier.verticalScroll(rememberScrollState()),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ OverscrollSample()
+ Spacer(Modifier.height(50.dp))
+ OverscrollWithDraggable_Before()
+ Spacer(Modifier.height(50.dp))
+ OverscrollWithDraggable_After()
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextOverflownSelection.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextOverflownSelection.kt
new file mode 100644
index 0000000..3055a47
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextOverflownSelection.kt
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.demos.text
+
+import android.content.ClipboardManager
+import android.content.Context.CLIPBOARD_SERVICE
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.selection.DisableSelection
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.material.LocalTextStyle
+import androidx.compose.material.RadioButton
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun TextOverflowedSelectionDemo() {
+ var overflow by remember { mutableStateOf(TextOverflow.Clip) }
+ val context = LocalContext.current
+ val clipboardManager = remember(context) {
+ context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
+ }
+ var copiedText by remember { mutableStateOf("") }
+
+ DisposableEffect(clipboardManager) {
+ val listener = ClipboardManager.OnPrimaryClipChangedListener {
+ copiedText = clipboardManager.read()
+ }
+ clipboardManager.addPrimaryClipChangedListener(listener)
+ onDispose {
+ clipboardManager.removePrimaryClipChangedListener(listener)
+ }
+ }
+
+ SelectionContainer {
+ Column {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ RadioButton(
+ selected = overflow == TextOverflow.Clip,
+ onClick = { overflow = TextOverflow.Clip })
+ Text(text = "Clip")
+ Spacer(modifier = Modifier.width(8.dp))
+ RadioButton(
+ selected = overflow == TextOverflow.Ellipsis,
+ onClick = { overflow = TextOverflow.Ellipsis })
+ Text(text = "Ellipsis")
+ }
+ DisableSelection {
+ Text(text = "Softwrap false, no maxLines")
+ }
+ OverflowToggleText(
+ text = loremIpsum(Language.Latin, wordCount = 50),
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(Color.Green),
+ softWrap = false,
+ overflow = overflow
+ )
+ DisableSelection {
+ Text(text = "Softwrap true, maxLines 1, in a row")
+ }
+ Row {
+ Box(modifier = Modifier.weight(1f), propagateMinConstraints = false) {
+ OverflowToggleText(
+ text = loremIpsum(Language.Latin, wordCount = 50),
+ modifier = Modifier
+ .background(Color.Green),
+ overflow = overflow,
+ maxLines = 1
+ )
+ }
+ Box(modifier = Modifier.weight(1f), propagateMinConstraints = false) {
+ OverflowToggleText(
+ text = loremIpsum(Language.Latin, wordCount = 50),
+ modifier = Modifier
+ .background(Color.Green),
+ overflow = overflow,
+ maxLines = 1
+ )
+ }
+ }
+ DisableSelection {
+ Text(text = "Softwrap true, height constrained, in a row")
+ }
+ Row {
+ Box(modifier = Modifier.weight(1f), propagateMinConstraints = false) {
+ OverflowToggleText(
+ text = loremIpsum(Language.Latin, wordCount = 50),
+ modifier = Modifier
+ .background(Color.Green)
+ .heightIn(max = 36.dp),
+ overflow = overflow
+ )
+ }
+ Box(modifier = Modifier.weight(1f), propagateMinConstraints = false) {
+ OverflowToggleText(
+ text = loremIpsum(Language.Latin, wordCount = 50),
+ modifier = Modifier
+ .background(Color.Green)
+ .heightIn(max = 36.dp),
+ overflow = overflow
+ )
+ }
+ }
+ DisableSelection {
+ Text(text = "Softwrap true, maxLines 1, half width")
+ }
+ OverflowToggleText(
+ text = loremIpsum(Language.Latin, wordCount = 50),
+ modifier = Modifier
+ .background(Color.Green)
+ .fillMaxWidth(0.5f),
+ overflow = overflow,
+ maxLines = 1
+ )
+ DisableSelection {
+ Text(text = "Softwrap true, maxLines 1")
+ }
+ OverflowToggleText(
+ text = loremIpsum(Language.Latin, wordCount = 50),
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(Color.Red),
+ maxLines = 1,
+ overflow = overflow
+ )
+
+ DisableSelection {
+ Text(
+ text = "BiDi, softwrap true, maxLines 1",
+ modifier = Modifier.padding(top = 16.dp)
+ )
+ }
+ OverflowToggleText(
+ text = loremIpsum(
+ Language.Latin,
+ wordCount = 3
+ ) + loremIpsum(Language.Arabic, wordCount = 20),
+ modifier = Modifier
+ .fillMaxWidth(),
+ maxLines = 1,
+ overflow = overflow
+ )
+
+ DisableSelection {
+ Text(text = "Copied Text", modifier = Modifier.padding(top = 16.dp))
+ }
+ TextField(value = copiedText, onValueChange = {}, modifier = Modifier.fillMaxWidth())
+ }
+ }
+}
+
+fun ClipboardManager.read(): String {
+ return primaryClip?.getItemAt(0)?.text.toString()
+}
+
+@Composable
+private fun OverflowToggleText(
+ text: String,
+ modifier: Modifier = Modifier,
+ color: Color = Color.Unspecified,
+ fontSize: TextUnit = TextUnit.Unspecified,
+ fontStyle: FontStyle? = null,
+ fontWeight: FontWeight? = null,
+ fontFamily: FontFamily? = null,
+ letterSpacing: TextUnit = TextUnit.Unspecified,
+ textDecoration: TextDecoration? = null,
+ textAlign: TextAlign? = null,
+ lineHeight: TextUnit = TextUnit.Unspecified,
+ overflow: TextOverflow = TextOverflow.Clip,
+ softWrap: Boolean = true,
+ maxLines: Int = Int.MAX_VALUE,
+ minLines: Int = 1,
+ onTextLayout: (TextLayoutResult) -> Unit = {},
+ style: TextStyle = LocalTextStyle.current
+) {
+ var toggleOverflow by remember(overflow) { mutableStateOf(overflow) }
+ Text(
+ text = text,
+ modifier = modifier.clickable {
+ toggleOverflow = when (toggleOverflow) {
+ TextOverflow.Clip -> TextOverflow.Ellipsis
+ TextOverflow.Ellipsis -> TextOverflow.Visible
+ TextOverflow.Visible -> TextOverflow.Clip
+ else -> TextOverflow.Clip
+ }
+ },
+ color = color,
+ fontSize = fontSize,
+ fontStyle = fontStyle,
+ fontWeight = fontWeight,
+ fontFamily = fontFamily,
+ letterSpacing = letterSpacing,
+ textDecoration = textDecoration,
+ textAlign = textAlign,
+ lineHeight = lineHeight,
+ overflow = toggleOverflow,
+ softWrap = if (toggleOverflow == TextOverflow.Visible) true else softWrap,
+ maxLines = if (toggleOverflow == TextOverflow.Visible) Int.MAX_VALUE else maxLines,
+ minLines = minLines,
+ onTextLayout = onTextLayout,
+ style = style
+ )
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LoremIpsum.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LoremIpsum.kt
index b28fb70..cf1da10 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LoremIpsum.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LoremIpsum.kt
@@ -28,7 +28,7 @@
language: Language = Language.Latin,
wordCount: Int = language.words.size
): String =
- language.words.joinToString(separator = " ", limit = wordCount)
+ language.words.joinToString(separator = " ", limit = wordCount, truncated = "")
private val LatinLipsum = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque a egestas nisi. Aenean
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index 258991f..758b084 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -114,6 +114,7 @@
listOf(
ComposableDemo("Text selection") { TextSelectionDemo() },
ComposableDemo("Text selection sample") { TextSelectionSample() },
+ ComposableDemo("Overflowed Selection") { TextOverflowedSelectionDemo() },
)
),
DemoCategory(
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt
index 696fa74..1a1e3f6 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt
@@ -23,6 +23,9 @@
import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
@@ -31,9 +34,11 @@
import androidx.compose.foundation.overscroll
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@@ -154,4 +159,80 @@
.overscroll(overscroll)
)
}
-}
\ No newline at end of file
+}
+
+@Sampled
+@Composable
+fun OverscrollWithDraggable_Before() {
+ var dragPosition by remember { mutableStateOf(0f) }
+ val minPosition = -1000f
+ val maxPosition = 1000f
+
+ val draggableState = rememberDraggableState { delta ->
+ val newPosition = (dragPosition + delta).coerceIn(minPosition, maxPosition)
+ dragPosition = newPosition
+ }
+
+ Box(
+ Modifier
+ .size(100.dp)
+ .draggable(draggableState, orientation = Orientation.Horizontal),
+ contentAlignment = Alignment.Center
+ ) {
+ Text("Drag position $dragPosition")
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Sampled
+@Composable
+fun OverscrollWithDraggable_After() {
+ var dragPosition by remember { mutableStateOf(0f) }
+ val minPosition = -1000f
+ val maxPosition = 1000f
+
+ val overscrollEffect = ScrollableDefaults.overscrollEffect()
+
+ val draggableState = rememberDraggableState { delta ->
+ // Horizontal, so convert the delta to a horizontal offset
+ val deltaAsOffset = Offset(delta, 0f)
+ // Wrap the original logic inside applyToScroll
+ overscrollEffect.applyToScroll(deltaAsOffset, NestedScrollSource.Drag) { remainingOffset ->
+ val remainingDelta = remainingOffset.x
+ val newPosition = (dragPosition + remainingDelta).coerceIn(minPosition, maxPosition)
+ // Calculate how much delta we have consumed
+ val consumed = newPosition - dragPosition
+ dragPosition = newPosition
+ // Return how much offset we consumed, so that we can show overscroll for what is left
+ Offset(consumed, 0f)
+ }
+ }
+
+ Box(
+ Modifier
+ // Draw overscroll on the box
+ .overscroll(overscrollEffect)
+ .size(100.dp)
+ .draggable(
+ draggableState,
+ orientation = Orientation.Horizontal,
+ onDragStopped = {
+ overscrollEffect.applyToFling(Velocity(it, 0f)) { velocity ->
+ if (dragPosition == minPosition || dragPosition == maxPosition) {
+ // If we are at the min / max bound, give overscroll all of the velocity
+ Velocity.Zero
+ } else {
+ // If we aren't at the min / max bound, consume all of the velocity so
+ // overscroll won't show. Normally in this case something like
+ // Modifier.scrollable would use the velocity to update the scroll state
+ // with a fling animation, but just do nothing to keep this simpler.
+ velocity
+ }
+ }
+ }
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Text("Drag position $dragPosition")
+ }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/StretchOverscrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/StretchOverscrollTest.kt
index 288ab0b..76e5125 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/StretchOverscrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/StretchOverscrollTest.kt
@@ -17,15 +17,19 @@
package androidx.compose.foundation
import android.os.Build
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.WithTouchSlop
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalViewConfiguration
-import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
@@ -36,11 +40,11 @@
import androidx.test.filters.SdkSuppress
import androidx.testutils.AnimationDurationScaleRule
import com.google.common.truth.Truth.assertThat
-import kotlin.math.abs
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+@OptIn(ExperimentalFoundationApi::class)
@MediumTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
@RunWith(AndroidJUnit4::class)
@@ -53,39 +57,213 @@
AnimationDurationScaleRule.createForAllTests(1f)
@Test
- fun stretchOverscroll_whenPulled_consumesOppositePreScroll() {
- val color = listOf(Color.Red, Color.Yellow, Color.Blue, Color.Green)
- val lazyState = LazyListState()
- animationScaleRule.setAnimationDurationScale(1f)
- var viewConfiguration: ViewConfiguration? = null
- rule.setContent {
- viewConfiguration = LocalViewConfiguration.current
- LazyRow(
- state = lazyState,
- modifier = Modifier.size(300.dp).testTag(OverscrollBox)
- ) {
- items(10) { index ->
- Box(Modifier.size(50.dp, 300.dp).background(color[index % color.size]))
- }
- }
- }
+ fun stretchOverscroll_whenPulled_consumesOppositePreScroll_pullLeft() {
+ val state = setStretchOverscrollContent(Orientation.Horizontal)
rule.onNodeWithTag(OverscrollBox).performTouchInput {
down(center)
- moveBy(Offset(-(200f + (viewConfiguration?.touchSlop ?: 0f)), 0f))
- // pull in the opposite direction. Since we pulled overscroll with positive delta
- // it will consume negative delta before scroll happens
- // assert in the ScrollableState lambda will check it
+ // Stretch by 200
+ moveBy(Offset(-200f, 0f))
+ // Pull 200 in the opposite direction - because we had 200 pixels of stretch before,
+ // this should only relax the existing overscroll, and not dispatch anything to the
+ // state
moveBy(Offset(200f, 0f))
- up()
}
rule.runOnIdle {
- // no scroll happened as it was consumed by the overscroll logic
- assertThat(abs(lazyState.firstVisibleItemScrollOffset)).isLessThan(2) // round error
- assertThat(lazyState.firstVisibleItemIndex).isEqualTo(0)
+ // All 200 should have been consumed by overscroll that was relaxing the existing
+ // stretch
+ assertThat(state.scrollPosition).isEqualTo(0f)
}
}
+
+ @Test
+ fun stretchOverscroll_whenPulledWithSmallDelta_doesNotConsumesOppositePreScroll_pullLeft() {
+ val state = setStretchOverscrollContent(Orientation.Horizontal)
+
+ rule.onNodeWithTag(OverscrollBox).performTouchInput {
+ down(center)
+ // Try and stretch by 0.4f
+ moveBy(Offset(-0.4f, 0f))
+ // Pull 200 in the opposite direction - overscroll should have ignored the 0.4f, and
+ // so all 200 should be dispatched to the state with nothing being consumed
+ moveBy(Offset(200f, 0f))
+ }
+
+ rule.runOnIdle {
+ // All 200 should be dispatched directly to the state
+ assertThat(state.scrollPosition).isEqualTo(200f)
+ }
+ }
+
+ @Test
+ fun stretchOverscroll_whenPulled_consumesOppositePreScroll_pullTop() {
+ val state = setStretchOverscrollContent(Orientation.Vertical)
+
+ rule.onNodeWithTag(OverscrollBox).performTouchInput {
+ down(center)
+ // Stretch by 200
+ moveBy(Offset(0f, -200f))
+ // Pull 200 in the opposite direction - because we had 200 pixels of stretch before,
+ // this should only relax the existing overscroll, and not dispatch anything to the
+ // state
+ moveBy(Offset(0f, 200f))
+ }
+
+ rule.runOnIdle {
+ // All 200 should have been consumed by overscroll that was relaxing the existing
+ // stretch
+ assertThat(state.scrollPosition).isEqualTo(0f)
+ }
+ }
+
+ @Test
+ fun stretchOverscroll_whenPulledWithSmallDelta_doesNotConsumesOppositePreScroll_pullTop() {
+ val state = setStretchOverscrollContent(Orientation.Vertical)
+
+ rule.onNodeWithTag(OverscrollBox).performTouchInput {
+ down(center)
+ // Try and stretch by 0.4f
+ moveBy(Offset(0f, -0.4f))
+ // Pull 200 in the opposite direction - overscroll should have ignored the 0.4f, and
+ // so all 200 should be dispatched to the state with nothing being consumed
+ moveBy(Offset(0f, 200f))
+ }
+
+ rule.runOnIdle {
+ // All 200 should be dispatched directly to the state
+ assertThat(state.scrollPosition).isEqualTo(200f)
+ }
+ }
+
+ @Test
+ fun stretchOverscroll_whenPulled_consumesOppositePreScroll_pullRight() {
+ val state = setStretchOverscrollContent(Orientation.Horizontal)
+
+ rule.onNodeWithTag(OverscrollBox).performTouchInput {
+ down(center)
+ // Stretch by 200 (the max scroll value is 1000)
+ moveBy(Offset(1200f, 0f))
+ // Pull 200 in the opposite direction - because we had 200 pixels of stretch before,
+ // this should only relax the existing overscroll, and not dispatch anything to the
+ // state
+ moveBy(Offset(-200f, 0f))
+ }
+
+ rule.runOnIdle {
+ // All -200 should have been consumed by overscroll that was relaxing the existing
+ // stretch
+ assertThat(state.scrollPosition).isEqualTo(1000f)
+ }
+ }
+
+ @Test
+ fun stretchOverscroll_whenPulledWithSmallDelta_doesNotConsumesOppositePreScroll_pullRight() {
+ val state = setStretchOverscrollContent(Orientation.Horizontal)
+
+ rule.onNodeWithTag(OverscrollBox).performTouchInput {
+ down(center)
+ // Try and stretch by 0.4f (the max scroll value is 1000)
+ moveBy(Offset(1000.4f, 0f))
+ // Pull 200 in the opposite direction - overscroll should have ignored the 0.4f, and
+ // so all -200 should be dispatched to the state with nothing being consumed
+ moveBy(Offset(-200f, 0f))
+ }
+
+ rule.runOnIdle {
+ // All -200 should be dispatched directly to the state
+ assertThat(state.scrollPosition).isEqualTo(800f)
+ }
+ }
+
+ @Test
+ fun stretchOverscroll_whenPulled_consumesOppositePreScroll_pullBottom() {
+ val state = setStretchOverscrollContent(Orientation.Vertical)
+
+ rule.onNodeWithTag(OverscrollBox).performTouchInput {
+ down(center)
+ // Stretch by 200 (the max scroll value is 1000)
+ moveBy(Offset(0f, 1200f))
+ // Pull 200 in the opposite direction - because we had 200 pixels of stretch before,
+ // this should only relax the existing overscroll, and not dispatch anything to the
+ // state
+ moveBy(Offset(0f, -200f))
+ }
+
+ rule.runOnIdle {
+ // All -200 should have been consumed by overscroll that was relaxing the existing
+ // stretch
+ assertThat(state.scrollPosition).isEqualTo(1000f)
+ }
+ }
+
+ @Test
+ fun stretchOverscroll_whenPulledWithSmallDelta_doesNotConsumesOppositePreScroll_pullBottom() {
+ val state = setStretchOverscrollContent(Orientation.Vertical)
+
+ rule.onNodeWithTag(OverscrollBox).performTouchInput {
+ down(center)
+ // Try and stretch by 0.4f (the max scroll value is 1000)
+ moveBy(Offset(0f, 1000.4f))
+ // Pull 200 in the opposite direction - overscroll should have ignored the 0.4f, and
+ // so all -200 should be dispatched to the state with nothing being consumed
+ moveBy(Offset(0f, -200f))
+ }
+
+ rule.runOnIdle {
+ // All -200 should be dispatched directly to the state
+ assertThat(state.scrollPosition).isEqualTo(800f)
+ }
+ }
+
+ private fun setStretchOverscrollContent(orientation: Orientation): TestScrollableState {
+ animationScaleRule.setAnimationDurationScale(1f)
+ val state = TestScrollableState()
+ rule.setContent {
+ WithTouchSlop(touchSlop = 0f) {
+ val overscroll = ScrollableDefaults.overscrollEffect()
+ Box(
+ Modifier
+ .testTag(OverscrollBox)
+ .size(100.dp)
+ .scrollable(
+ state = state,
+ orientation = orientation,
+ overscrollEffect = overscroll
+ )
+ .overscroll(overscroll)
+ )
+ }
+ }
+ return state
+ }
+}
+
+/**
+ * Returns a default [ScrollableState] with a [scrollPosition] clamped between 0f and 1000f.
+ */
+private class TestScrollableState : ScrollableState {
+ var scrollPosition by mutableStateOf(0f)
+ private set
+
+ // Using ScrollableState here instead of ScrollState as ScrollState will automatically round to
+ // an int, and we need to assert floating point values
+ private val scrollableState = ScrollableState {
+ val newPosition = (scrollPosition + it).coerceIn(0f, 1000f)
+ val consumed = newPosition - scrollPosition
+ scrollPosition = newPosition
+ consumed
+ }
+
+ override suspend fun scroll(
+ scrollPriority: MutatePriority,
+ block: suspend ScrollScope.() -> Unit
+ ) = scrollableState.scroll(scrollPriority, block)
+
+ override fun dispatchRawDelta(delta: Float) = scrollableState.dispatchRawDelta(delta)
+
+ override val isScrollInProgress: Boolean
+ get() = scrollableState.isScrollInProgress
}
private const val OverscrollBox = "box"
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PageLayoutPositionOnScrollingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PageLayoutPositionOnScrollingTest.kt
new file mode 100644
index 0000000..8d25cb7
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PageLayoutPositionOnScrollingTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.filters.LargeTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+internal class PageLayoutPositionOnScrollingTest(
+ val config: ParamConfig
+) : BasePagerTest(config) {
+
+ @Test
+ fun swipeForwardAndBackward_verifyPagesAreLaidOutCorrectly() {
+ // Arrange
+ val state = PagerState()
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+ val delta = pagerSize * 0.4f * scrollForwardSign
+
+ // Act and Assert - forward
+ repeat(DefaultAnimationRepetition) {
+ rule.onNodeWithTag(it.toString()).assertIsDisplayed()
+ confirmPageIsInCorrectPosition(it)
+ rule.onNodeWithTag(it.toString()).performTouchInput {
+ swipeWithVelocityAcrossMainAxis(
+ with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
+ delta
+ )
+ }
+ rule.waitForIdle()
+ }
+
+ // Act - backward
+ repeat(DefaultAnimationRepetition) {
+ val countDown = DefaultAnimationRepetition - it
+ rule.onNodeWithTag(countDown.toString()).assertIsDisplayed()
+ confirmPageIsInCorrectPosition(countDown)
+ rule.onNodeWithTag(countDown.toString()).performTouchInput {
+ swipeWithVelocityAcrossMainAxis(
+ with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
+ delta * -1f
+ )
+ }
+ rule.waitForIdle()
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() = mutableListOf<ParamConfig>().apply {
+ for (orientation in TestOrientation) {
+ for (pageSpacing in TestPageSpacing) {
+ for (reverseLayout in TestReverseLayout) {
+ for (layoutDirection in TestLayoutDirection) {
+ for (contentPadding in testContentPaddings(orientation)) {
+ add(
+ ParamConfig(
+ orientation = orientation,
+ pageSpacing = pageSpacing,
+ mainAxisContentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ layoutDirection = layoutDirection
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
index d8d56a8..6d85e72 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
@@ -25,7 +25,6 @@
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.unit.dp
import androidx.test.filters.LargeTest
-import androidx.test.filters.RequiresDevice
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.Test
@@ -40,75 +39,6 @@
) : BasePagerTest(config) {
@Test
- fun swipePageTowardsEdge_shouldNotMove() {
- // Arrange
- val state = PagerState()
- createPager(state = state, modifier = Modifier.fillMaxSize())
- val delta = pagerSize * 0.4f * scrollForwardSign
-
- // Act - backward
- rule.onNodeWithTag("0").performTouchInput {
- swipeWithVelocityAcrossMainAxis(
- with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
- delta * -1.0f
- )
- }
- rule.waitForIdle()
-
- // Assert
- rule.onNodeWithTag("0").assertIsDisplayed()
- confirmPageIsInCorrectPosition(0)
-
- // Act - forward
- onPager().performTouchInput {
- swipeWithVelocityAcrossMainAxis(
- with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
- delta
- )
- }
- rule.waitForIdle()
-
- // Assert
- rule.onNodeWithTag("1").assertIsDisplayed()
- confirmPageIsInCorrectPosition(1)
- }
-
- @Test
- fun swipeForwardAndBackward_verifyPagesAreLaidOutCorrectly() {
- // Arrange
- val state = PagerState()
- createPager(state = state, modifier = Modifier.fillMaxSize())
- val delta = pagerSize * 0.4f * scrollForwardSign
-
- // Act and Assert - forward
- repeat(DefaultAnimationRepetition) {
- rule.onNodeWithTag(it.toString()).assertIsDisplayed()
- confirmPageIsInCorrectPosition(it)
- rule.onNodeWithTag(it.toString()).performTouchInput {
- swipeWithVelocityAcrossMainAxis(
- with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
- delta
- )
- }
- rule.waitForIdle()
- }
-
- // Act - backward
- repeat(DefaultAnimationRepetition) {
- val countDown = DefaultAnimationRepetition - it
- rule.onNodeWithTag(countDown.toString()).assertIsDisplayed()
- confirmPageIsInCorrectPosition(countDown)
- rule.onNodeWithTag(countDown.toString()).performTouchInput {
- swipeWithVelocityAcrossMainAxis(
- with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
- delta * -1f
- )
- }
- rule.waitForIdle()
- }
- }
-
- @Test
fun swipeWithLowVelocity_shouldBounceBack() {
// Arrange
val state = PagerState(5)
@@ -177,7 +107,6 @@
confirmPageIsInCorrectPosition(5)
}
- @RequiresDevice // b/266452930
@Test
fun swipeWithHighVelocity_overHalfPage_shouldGoToNextPage() {
// Arrange
@@ -349,22 +278,13 @@
@Parameterized.Parameters(name = "{0}")
fun params() = mutableListOf<ParamConfig>().apply {
for (orientation in TestOrientation) {
- for (reverseLayout in TestReverseLayout) {
- for (layoutDirection in TestLayoutDirection) {
- for (pageSpacing in TestPageSpacing) {
- for (contentPadding in testContentPaddings(orientation)) {
- add(
- ParamConfig(
- orientation = orientation,
- reverseLayout = reverseLayout,
- layoutDirection = layoutDirection,
- pageSpacing = pageSpacing,
- mainAxisContentPadding = contentPadding
- )
- )
- }
- }
- }
+ for (pageSpacing in TestPageSpacing) {
+ add(
+ ParamConfig(
+ orientation = orientation,
+ pageSpacing = pageSpacing
+ )
+ )
}
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerSwipeEdgeTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerSwipeEdgeTest.kt
new file mode 100644
index 0000000..5dd952a
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerSwipeEdgeTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.filters.LargeTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+internal class PagerSwipeEdgeTest(
+ val config: ParamConfig
+) : BasePagerTest(config) {
+
+ @Test
+ fun swipePageTowardsEdge_shouldNotMove() {
+ // Arrange
+ val state = PagerState()
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+ val delta = pagerSize * 0.4f * scrollForwardSign
+
+ // Act - backward
+ rule.onNodeWithTag("0").performTouchInput {
+ swipeWithVelocityAcrossMainAxis(
+ with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
+ delta * -1.0f
+ )
+ }
+ rule.waitForIdle()
+
+ // Assert
+ rule.onNodeWithTag("0").assertIsDisplayed()
+ confirmPageIsInCorrectPosition(0)
+
+ // Act - forward
+ onPager().performTouchInput {
+ swipeWithVelocityAcrossMainAxis(
+ with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
+ delta
+ )
+ }
+ rule.waitForIdle()
+
+ // Assert
+ rule.onNodeWithTag("1").assertIsDisplayed()
+ confirmPageIsInCorrectPosition(1)
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() = mutableListOf<ParamConfig>().apply {
+ for (orientation in TestOrientation) {
+ for (reverseLayout in TestReverseLayout) {
+ for (layoutDirection in TestLayoutDirection) {
+ add(
+ ParamConfig(
+ orientation = orientation,
+ reverseLayout = reverseLayout,
+ layoutDirection = layoutDirection
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests.kt
index 1e01af6..1c6dfb1 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests.kt
@@ -67,7 +67,8 @@
text: String,
modifier: Modifier,
style: TextStyle,
- onTextLayout: (TextLayoutResult) -> Unit
+ onTextLayout: (TextLayoutResult) -> Unit,
+ maxLines: Int
)
@Test
@@ -272,8 +273,9 @@
text: String,
modifier: Modifier,
style: TextStyle = TextStyle.Default,
- onTextLayout: (TextLayoutResult) -> Unit = {}
- ) = TestContent(text, modifier, style, onTextLayout)
+ onTextLayout: (TextLayoutResult) -> Unit = {},
+ maxLines: Int = Int.MAX_VALUE
+ ) = TestContent(text, modifier, style, onTextLayout, maxLines)
protected fun checkMagnifierAppears_whileHandleTouched(handle: Handle) {
rule.setContent {
@@ -486,6 +488,35 @@
.of(magnifierInitialPosition.y + lineHeight)
}
+ protected fun checkMagnifierAsHandleGoesOutOfBoundsUsingMaxLines(handle: Handle) {
+ var lineHeight = 0f
+ rule.setContent {
+ Content(
+ "aaaa aaaa aaaa\naaaa aaaa aaaa",
+ Modifier
+ // Center the text to give the magnifier lots of room to move.
+ .fillMaxSize()
+ .wrapContentSize()
+ .testTag(tag),
+ onTextLayout = { lineHeight = it.getLineBottom(0) - it.getLineTop(0) },
+ maxLines = 1
+ )
+ }
+
+ showHandle(handle)
+
+ // Touch the handle to show the magnifier.
+ rule.onNode(isSelectionHandle(handle))
+ .performTouchInput { down(center) }
+
+ // Drag the handle down - the magnifier should follow.
+ val dragDistance = Offset(0f, lineHeight)
+ rule.onNode(isSelectionHandle(handle))
+ .performTouchInput { movePastSlopBy(dragDistance) }
+
+ assertNoMagnifierExists()
+ }
+
protected fun checkMagnifierDoesNotFollowHandleVerticallyWithinLine(handle: Handle) {
val dragDistance = Offset(0f, 1f)
rule.setContent {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
index 3d29611..28acdca 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
@@ -16,21 +16,20 @@
package androidx.compose.foundation.text.selection
-import androidx.activity.ComponentActivity
import androidx.compose.foundation.text.InternalFoundationTextApi
import androidx.compose.foundation.text.TEST_FONT_FAMILY
import androidx.compose.foundation.text.TextDelegate
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
@@ -42,122 +41,114 @@
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
-import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
class MultiWidgetSelectionDelegateTest {
- @get:Rule
- val composeTestRule = createAndroidComposeRule<ComponentActivity>()
private val fontFamily = TEST_FONT_FAMILY
private val context = InstrumentationRegistry.getInstrumentation().context
private val defaultDensity = Density(density = 1f)
- @OptIn(ExperimentalTextApi::class)
private val fontFamilyResolver = createFontFamilyResolver(context)
@Test
fun getHandlePosition_StartHandle_invalid() {
- composeTestRule.setContent {
- val text = "hello world\n"
- val fontSize = 20.sp
+ val text = "hello world\n"
+ val fontSize = 20.sp
- val layoutResult = simpleTextLayout(
- text = text,
- fontSize = fontSize,
- density = defaultDensity
- )
+ val layoutResult = simpleTextLayout(
+ text = text,
+ fontSize = fontSize,
+ density = defaultDensity
+ )
- val layoutCoordinates = mock<LayoutCoordinates>()
- whenever(layoutCoordinates.isAttached).thenReturn(true)
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
- val selectableId = 1L
- val selectable = MultiWidgetSelectionDelegate(
- selectableId = selectableId,
- coordinatesCallback = { layoutCoordinates },
- layoutResultCallback = { layoutResult }
- )
+ val selectableId = 1L
+ val selectable = MultiWidgetSelectionDelegate(
+ selectableId = selectableId,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
- val selectableInvalidId = 2L
- val startOffset = text.indexOf('h')
- val endOffset = text.indexOf('o')
+ val selectableInvalidId = 2L
+ val startOffset = text.indexOf('h')
+ val endOffset = text.indexOf('o')
- val selection = Selection(
- start = Selection.AnchorInfo(
- direction = ResolvedTextDirection.Ltr,
- offset = startOffset,
- selectableId = selectableInvalidId
- ),
- end = Selection.AnchorInfo(
- direction = ResolvedTextDirection.Ltr,
- offset = endOffset,
- selectableId = selectableInvalidId
- ),
- handlesCrossed = false
- )
+ val selection = Selection(
+ start = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Ltr,
+ offset = startOffset,
+ selectableId = selectableInvalidId
+ ),
+ end = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Ltr,
+ offset = endOffset,
+ selectableId = selectableInvalidId
+ ),
+ handlesCrossed = false
+ )
- // Act.
- val coordinates = selectable.getHandlePosition(
- selection = selection,
- isStartHandle = true
- )
+ // Act.
+ val coordinates = selectable.getHandlePosition(
+ selection = selection,
+ isStartHandle = true
+ )
- // Assert.
- assertThat(coordinates).isEqualTo(Offset.Zero)
- }
+ // Assert.
+ assertThat(coordinates).isEqualTo(Offset.Zero)
}
@Test
fun getHandlePosition_EndHandle_invalid() {
- composeTestRule.setContent {
- val text = "hello world\n"
- val fontSize = 20.sp
+ val text = "hello world\n"
+ val fontSize = 20.sp
- val layoutResult = simpleTextLayout(
- text = text,
- fontSize = fontSize,
- density = defaultDensity
- )
+ val layoutResult = simpleTextLayout(
+ text = text,
+ fontSize = fontSize,
+ density = defaultDensity
+ )
- val layoutCoordinates = mock<LayoutCoordinates>()
- whenever(layoutCoordinates.isAttached).thenReturn(true)
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
- val selectableId = 1L
- val selectable = MultiWidgetSelectionDelegate(
- selectableId = selectableId,
- coordinatesCallback = { layoutCoordinates },
- layoutResultCallback = { layoutResult }
- )
+ val selectableId = 1L
+ val selectable = MultiWidgetSelectionDelegate(
+ selectableId = selectableId,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
- val selectableInvalidId = 2L
- val startOffset = text.indexOf('h')
- val endOffset = text.indexOf('o')
+ val selectableInvalidId = 2L
+ val startOffset = text.indexOf('h')
+ val endOffset = text.indexOf('o')
- val selection = Selection(
- start = Selection.AnchorInfo(
- direction = ResolvedTextDirection.Ltr,
- offset = startOffset,
- selectableId = selectableInvalidId
- ),
- end = Selection.AnchorInfo(
- direction = ResolvedTextDirection.Ltr,
- offset = endOffset,
- selectableId = selectableInvalidId
- ),
- handlesCrossed = false
- )
+ val selection = Selection(
+ start = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Ltr,
+ offset = startOffset,
+ selectableId = selectableInvalidId
+ ),
+ end = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Ltr,
+ offset = endOffset,
+ selectableId = selectableInvalidId
+ ),
+ handlesCrossed = false
+ )
- // Act.
- val coordinates = selectable.getHandlePosition(
- selection = selection,
- isStartHandle = false
- )
+ // Act.
+ val coordinates = selectable.getHandlePosition(
+ selection = selection,
+ isStartHandle = false
+ )
- // Assert.
- assertThat(coordinates).isEqualTo(Offset.Zero)
- }
+ // Assert.
+ assertThat(coordinates).isEqualTo(Offset.Zero)
}
@Test
@@ -522,6 +513,58 @@
}
@Test
+ fun getHandlePosition_EndHandle_not_cross_ltr_overflowed() {
+ val text = "hello\nworld"
+ val fontSize = 20.sp
+ val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
+
+ val layoutResult = simpleTextLayout(
+ text = text,
+ fontSize = fontSize,
+ density = defaultDensity,
+ maxLines = 1
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectableId = 1L
+ val selectable = MultiWidgetSelectionDelegate(
+ selectableId = selectableId,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val startOffset = text.indexOf('h')
+ val endOffset = text.indexOf('r')
+
+ val selection = Selection(
+ start = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Ltr,
+ offset = startOffset,
+ selectableId = selectableId
+ ),
+ end = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Ltr,
+ offset = endOffset,
+ selectableId = selectableId
+ ),
+ handlesCrossed = false
+ )
+
+ // Act.
+ val coordinates = selectable.getHandlePosition(
+ selection = selection,
+ isStartHandle = false
+ )
+
+ // Assert.
+ assertThat(coordinates).isEqualTo(
+ Offset(fontSizeInPx * 5, fontSizeInPx) // the last offset in the first line
+ )
+ }
+
+ @Test
fun getHandlePosition_EndHandle_cross_ltr() {
val text = "hello world\n"
val fontSize = 20.sp
@@ -573,6 +616,58 @@
}
@Test
+ fun getHandlePosition_EndHandle_cross_ltr_overflowed() {
+ val text = "hello\nworld"
+ val fontSize = 20.sp
+ val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
+
+ val layoutResult = simpleTextLayout(
+ text = text,
+ fontSize = fontSize,
+ density = defaultDensity,
+ maxLines = 1
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectableId = 1L
+ val selectable = MultiWidgetSelectionDelegate(
+ selectableId = selectableId,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val startOffset = text.indexOf('r')
+ val endOffset = text.indexOf('w')
+
+ val selection = Selection(
+ start = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Ltr,
+ offset = startOffset,
+ selectableId = selectableId
+ ),
+ end = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Ltr,
+ offset = endOffset,
+ selectableId = selectableId
+ ),
+ handlesCrossed = true
+ )
+
+ // Act.
+ val coordinates = selectable.getHandlePosition(
+ selection = selection,
+ isStartHandle = false
+ )
+
+ // Assert.
+ assertThat(coordinates).isEqualTo(
+ Offset((fontSizeInPx * 5), fontSizeInPx)
+ )
+ }
+
+ @Test
fun getHandlePosition_EndHandle_not_cross_rtl() {
val text = "\u05D0\u05D1\u05D2 \u05D3\u05D4\u05D5\n"
val fontSize = 20.sp
@@ -675,6 +770,106 @@
}
@Test
+ fun getHandlePosition_EndHandle_not_cross_rtl_overflowed() {
+ val text = "\u05D0\u05D1\u05D2\n\u05D3\u05D4\u05D5"
+ val fontSize = 20.sp
+ val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
+
+ val layoutResult = simpleTextLayout(
+ text = text,
+ fontSize = fontSize,
+ density = defaultDensity,
+ maxLines = 1
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectableId = 1L
+ val selectable = MultiWidgetSelectionDelegate(
+ selectableId = selectableId,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val startOffset = text.indexOf('\u05D1')
+ val endOffset = text.indexOf('\u05D5')
+
+ val selection = Selection(
+ start = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Rtl,
+ offset = startOffset,
+ selectableId = selectableId
+ ),
+ end = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Rtl,
+ offset = endOffset,
+ selectableId = selectableId
+ ),
+ handlesCrossed = false
+ )
+
+ // Act.
+ val coordinates = selectable.getHandlePosition(
+ selection = selection,
+ isStartHandle = false
+ )
+
+ // Assert.
+ assertThat(coordinates).isEqualTo(Offset(0f, fontSizeInPx))
+ }
+
+ @Test
+ fun getHandlePosition_EndHandle_cross_rtl_overflowed() {
+ val text = "\u05D0\u05D1\u05D2\n\u05D3\u05D4\u05D5"
+ val fontSize = 20.sp
+ val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
+
+ val layoutResult = simpleTextLayout(
+ text = text,
+ fontSize = fontSize,
+ density = defaultDensity,
+ maxLines = 1
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectableId = 1L
+ val selectable = MultiWidgetSelectionDelegate(
+ selectableId = selectableId,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val startOffset = text.indexOf('\u05D5')
+ val endOffset = text.indexOf('\u05D3')
+
+ val selection = Selection(
+ start = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Rtl,
+ offset = startOffset,
+ selectableId = selectableId
+ ),
+ end = Selection.AnchorInfo(
+ direction = ResolvedTextDirection.Rtl,
+ offset = endOffset,
+ selectableId = selectableId
+ ),
+ handlesCrossed = true
+ )
+
+ // Act.
+ val coordinates = selectable.getHandlePosition(
+ selection = selection,
+ isStartHandle = false
+ )
+
+ // Assert.
+ assertThat(coordinates).isEqualTo(Offset((fontSizeInPx * 3), fontSizeInPx))
+ }
+
+ @Test
fun getHandlePosition_EndHandle_not_cross_bidi() {
val textLtr = "Hello"
val textRtl = "\u05D0\u05D1\u05D2"
@@ -965,8 +1160,7 @@
val lineRange = selectable.getRangeOfLineContaining(0)
// Assert.
- assertThat(lineRange.start).isEqualTo(0)
- assertThat(lineRange.end).isEqualTo(5)
+ assertThat(lineRange).isEqualTo(TextRange(0, 5))
}
@Test
@@ -991,8 +1185,7 @@
val lineRange = selectable.getRangeOfLineContaining(7)
// Assert.
- assertThat(lineRange.start).isEqualTo(6)
- assertThat(lineRange.end).isEqualTo(11)
+ assertThat(lineRange).isEqualTo(TextRange(6, 11))
}
@Test
@@ -1017,8 +1210,7 @@
val lineRange = selectable.getRangeOfLineContaining(-1)
// Assert.
- assertThat(lineRange.start).isEqualTo(0)
- assertThat(lineRange.end).isEqualTo(5)
+ assertThat(lineRange).isEqualTo(TextRange(0, 5))
}
@Test
@@ -1043,8 +1235,7 @@
val lineRange = selectable.getRangeOfLineContaining(Int.MAX_VALUE)
// Assert.
- assertThat(lineRange.start).isEqualTo(6)
- assertThat(lineRange.end).isEqualTo(11)
+ assertThat(lineRange).isEqualTo(TextRange(6, 11))
}
@Test
@@ -1069,8 +1260,7 @@
val lineRange = selectable.getRangeOfLineContaining(5)
// Assert.
- assertThat(lineRange.start).isEqualTo(0)
- assertThat(lineRange.end).isEqualTo(5)
+ assertThat(lineRange).isEqualTo(TextRange(0, 5))
}
@Test
@@ -1095,8 +1285,7 @@
val lineRange = selectable.getRangeOfLineContaining(5)
// Assert.
- assertThat(lineRange.start).isEqualTo(0)
- assertThat(lineRange.end).isEqualTo(0)
+ assertThat(lineRange).isEqualTo(TextRange.Zero)
}
@Test
@@ -1121,8 +1310,532 @@
val lineRange = selectable.getRangeOfLineContaining(6)
// Assert.
- assertThat(lineRange.start).isEqualTo(6)
- assertThat(lineRange.end).isEqualTo(6)
+ assertThat(lineRange).isEqualTo(TextRange(6, 6))
+ }
+
+ @Test
+ fun getRangeOfLineContaining_overflowed_returnsLastVisibleLine() {
+ val text = "hello\nworld"
+
+ val layoutResult = simpleTextLayout(
+ text = text,
+ density = defaultDensity,
+ maxLines = 1
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ // Act.
+ val lineRange = selectable.getRangeOfLineContaining(6)
+
+ // Assert.
+ assertThat(lineRange).isEqualTo(TextRange(0, 5))
+ }
+
+ @Test
+ fun getRangeOfLineContaining_overflowedDueToMaxHeight_returnsLastVisibleLine() {
+ val text = "hello\nworld"
+ val fontSize = 20.sp
+
+ val layoutResult = simpleTextLayout(
+ text = text,
+ density = defaultDensity,
+ fontSize = fontSize,
+ constraints = Constraints(maxHeight = with(defaultDensity) { fontSize.roundToPx() } * 1)
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ // Act.
+ val lineRange = selectable.getRangeOfLineContaining(6)
+
+ // Assert.
+ assertThat(lineRange).isEqualTo(TextRange(0, 5))
+ }
+
+ @Test
+ fun getLastVisibleOffset_everythingVisible_returnsTextLength() {
+ val text = "hello\nworld"
+
+ val layoutResult = constrainedTextLayout(
+ text = text
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ assertThat(lastVisibleOffset).isEqualTo(text.length)
+ }
+
+ @Test
+ fun getLastVisibleOffset_changesWhenTextLayoutChanges() {
+ val text = "hello\nworld"
+
+ var layoutResult = constrainedTextLayout(text = text)
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ assertThat(selectable.getLastVisibleOffset()).isEqualTo(text.length)
+
+ layoutResult = constrainedTextLayout(text = "$text$text")
+
+ assertThat(selectable.getLastVisibleOffset()).isEqualTo(text.length * 2)
+ }
+
+ // start = maxLines 1
+ // start = clip
+ // start = enabled soft wrap
+ @Test
+ fun getLastVisibleOffset_maxLines1_clip_enabledSoftwrap_multiLineContent() {
+ val text = "hello\nworld"
+
+ val layoutResult = constrainedTextLayout(
+ text = text,
+ overflow = TextOverflow.Clip,
+ maxLines = 1,
+ softWrap = true
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ assertThat(lastVisibleOffset).isEqualTo(5)
+ }
+
+ @Test
+ fun getLastVisibleOffset_maxLines1_clip_enabledSoftwrap_singleLineContent() {
+ val text = "hello world"
+
+ val layoutResult = constrainedTextLayout(
+ text = text,
+ maxLines = 1,
+ overflow = TextOverflow.Clip,
+ softWrap = true,
+ widthInCharacters = 10
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ assertThat(lastVisibleOffset).isEqualTo(5)
+ }
+
+ // start = disabled soft wrap
+ @Test
+ fun getLastVisibleOffset_maxLines1_clip_disabledSoftwrap_multiLineContent() {
+ val text = "hello\nworld"
+
+ val layoutResult = constrainedTextLayout(
+ text = text,
+ maxLines = 1,
+ overflow = TextOverflow.Clip,
+ softWrap = false
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ assertThat(lastVisibleOffset).isEqualTo(5)
+ }
+
+ @Test
+ fun getLastVisibleOffset_maxLines1_clip_disabledSoftwrap_singleLineContent() {
+ val text = "hello world ".repeat(10)
+
+ val layoutResult = constrainedTextLayout(
+ text = text,
+ maxLines = 1,
+ overflow = TextOverflow.Clip,
+ softWrap = false,
+ widthInCharacters = 10
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ assertThat(lastVisibleOffset).isEqualTo(text.length)
+ }
+
+ // start = ellipsis
+ // start = enabled soft wrap
+ @Test
+ fun getLastVisibleOffset_maxLines1_ellipsis_enabledSoftwrap_multiLineContent() {
+ val text = "hello\nworld"
+
+ val layoutResult = constrainedTextLayout(
+ text = text,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ softWrap = true,
+ widthInCharacters = 4
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ assertThat(lastVisibleOffset).isEqualTo(3)
+ }
+
+ @Test
+ fun getLastVisibleOffset_maxLines1_ellipsis_enabledSoftwrap_singleLineContent() {
+ val text = "hello world ".repeat(10)
+
+ val layoutResult = constrainedTextLayout(
+ text = text,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ softWrap = true,
+ widthInCharacters = 10
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ assertThat(lastVisibleOffset).isEqualTo(9)
+ }
+
+ // start = disabled soft wrap
+ @Test
+ fun getLastVisibleOffset_maxLines1_ellipsis_disabledSoftwrap_multiLineContent() {
+ val text = "hello\nworld"
+
+ val layoutResult = constrainedTextLayout(
+ text = text,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ softWrap = false,
+ widthInCharacters = 5
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ // first line will include an ellipsis before line break
+ assertThat(lastVisibleOffset).isEqualTo(4)
+ }
+
+ @Test
+ fun getLastVisibleOffset_maxLines1_ellipsis_disabledSoftwrap_singleLineContent() {
+ val text = "hello world ".repeat(10)
+
+ val layoutResult = constrainedTextLayout(
+ text = text,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ softWrap = false,
+ widthInCharacters = 20
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ assertThat(lastVisibleOffset).isEqualTo(19)
+ }
+
+ // start = height constrained
+ // start = clip
+ // start = enabled soft wrap
+ @Test
+ fun getLastVisibleOffset_limitHeight_clip_enabledSoftwrap_multiLineContent() {
+ val text = "hello\nworld"
+
+ val layoutResult = constrainedTextLayout(
+ text = text,
+ overflow = TextOverflow.Clip,
+ softWrap = true,
+ maxHeightInLines = 1
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ assertThat(lastVisibleOffset).isEqualTo(5)
+ }
+
+ @Test
+ fun getLastVisibleOffset_limitHeight_clip_enabledSoftwrap_singleLineContent() {
+ val text = "helloworld helloworld helloworld"
+
+ val layoutResult = constrainedTextLayout(
+ text = text,
+ overflow = TextOverflow.Clip,
+ softWrap = true,
+ widthInCharacters = 10,
+ maxHeightInLines = 2
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ assertThat(lastVisibleOffset).isEqualTo(21)
+ }
+
+ // start = disabled soft wrap
+ @Test
+ fun getLastVisibleOffset_limitHeight_clip_disabledSoftwrap_multiLineContent() {
+ val text = "hello\nworld"
+
+ val layoutResult = constrainedTextLayout(
+ text = text,
+ overflow = TextOverflow.Clip,
+ softWrap = false,
+ maxHeightInLines = 1
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ assertThat(lastVisibleOffset).isEqualTo(5)
+ }
+
+ @Test
+ fun getLastVisibleOffset_limitHeight_clip_disabledSoftwrap_singleLineContent() {
+ val text = "hello world ".repeat(10)
+
+ val layoutResult = constrainedTextLayout(
+ text = text,
+ overflow = TextOverflow.Clip,
+ softWrap = false,
+ widthInCharacters = 10,
+ maxHeightInLines = 1
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ assertThat(lastVisibleOffset).isEqualTo(text.length)
+ }
+
+ // start = ellipsis
+ // start = enabled soft wrap
+ @Test
+ fun getLastVisibleOffset_limitHeight_ellipsis_enabledSoftwrap_multiLineContent() {
+ val text = "hello\nworld\nhello\nworld"
+
+ val layoutResult = constrainedTextLayout(
+ text = text,
+ overflow = TextOverflow.Ellipsis,
+ softWrap = true,
+ widthInCharacters = 10,
+ maxHeightInLines = 2,
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ assertThat(lastVisibleOffset).isEqualTo(11)
+ }
+
+ @Test
+ fun getLastVisibleOffset_limitHeight_ellipsis_enabledSoftwrap_singleLineContent() {
+ val text = "hello world ".repeat(10)
+
+ val layoutResult = constrainedTextLayout(
+ text = text,
+ overflow = TextOverflow.Ellipsis,
+ softWrap = true,
+ widthInCharacters = 10,
+ maxHeightInLines = 1
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ assertThat(lastVisibleOffset).isEqualTo(9)
+ }
+
+ // start = disabled soft wrap
+ @Test
+ fun getLastVisibleOffset_limitHeight_ellipsis_disabledSoftwrap_multiLineContent() {
+ val text = "hello\nworld"
+
+ val layoutResult = constrainedTextLayout(
+ text = text,
+ overflow = TextOverflow.Ellipsis,
+ softWrap = false,
+ maxHeightInLines = 1
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ // first line will include an ellipsis before line break
+ assertThat(lastVisibleOffset).isEqualTo(5)
+ }
+
+ @Test
+ fun getLastVisibleOffset_limitHeight_ellipsis_disabledSoftwrap_singleLineContent() {
+ val text = "hello world ".repeat(10)
+
+ val layoutResult = constrainedTextLayout(
+ text = text,
+ overflow = TextOverflow.Ellipsis,
+ softWrap = false,
+ widthInCharacters = 20,
+ maxHeightInLines = 1
+ )
+
+ val layoutCoordinates = mock<LayoutCoordinates>()
+ whenever(layoutCoordinates.isAttached).thenReturn(true)
+
+ val selectable = MultiWidgetSelectionDelegate(
+ 1,
+ coordinatesCallback = { layoutCoordinates },
+ layoutResultCallback = { layoutResult }
+ )
+
+ val lastVisibleOffset = selectable.getLastVisibleOffset()
+
+ assertThat(lastVisibleOffset).isEqualTo(19)
}
@Test
@@ -2647,7 +3360,9 @@
private fun simpleTextLayout(
text: String = "",
fontSize: TextUnit = TextUnit.Unspecified,
- density: Density
+ density: Density,
+ maxLines: Int = Int.MAX_VALUE,
+ constraints: Constraints = Constraints()
): TextLayoutResult {
val spanStyle = SpanStyle(fontSize = fontSize, fontFamily = fontFamily)
val annotatedString = AnnotatedString(text, spanStyle)
@@ -2655,7 +3370,42 @@
text = annotatedString,
style = TextStyle(),
density = density,
+ maxLines = maxLines,
fontFamilyResolver = fontFamilyResolver
- ).layout(Constraints(), LayoutDirection.Ltr)
+ ).layout(constraints, LayoutDirection.Ltr)
+ }
+
+ @OptIn(InternalFoundationTextApi::class)
+ private fun constrainedTextLayout(
+ text: String = "",
+ fontSize: TextUnit = 20.sp,
+ density: Density = defaultDensity,
+ maxLines: Int = Int.MAX_VALUE,
+ overflow: TextOverflow = TextOverflow.Clip,
+ softWrap: Boolean = true,
+ widthInCharacters: Int = 20,
+ maxHeightInLines: Int = Int.MAX_VALUE
+ ): TextLayoutResult {
+ val spanStyle = SpanStyle(fontSize = fontSize, fontFamily = fontFamily)
+ val annotatedString = AnnotatedString(text, spanStyle)
+ val width = with(density) { fontSize.roundToPx() } * widthInCharacters
+ val constraints = Constraints(
+ minWidth = width,
+ maxWidth = width,
+ maxHeight = if (maxHeightInLines == Int.MAX_VALUE) {
+ Int.MAX_VALUE
+ } else {
+ with(density) { fontSize.roundToPx() } * maxHeightInLines
+ }
+ )
+ return TextDelegate(
+ text = annotatedString,
+ style = TextStyle(),
+ density = density,
+ fontFamilyResolver = fontFamilyResolver,
+ maxLines = maxLines,
+ overflow = overflow,
+ softWrap = softWrap
+ ).layout(constraints, LayoutDirection.Ltr)
}
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest.kt
index ad4fccb..00dba57 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest.kt
@@ -17,13 +17,16 @@
package androidx.compose.foundation.text.selection
import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.Handle
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import androidx.test.filters.RequiresDevice
import androidx.test.filters.SdkSuppress
+import org.junit.Test
import org.junit.runner.RunWith
@MediumTest
@@ -36,10 +39,23 @@
text: String,
modifier: Modifier,
style: TextStyle,
- onTextLayout: (TextLayoutResult) -> Unit
+ onTextLayout: (TextLayoutResult) -> Unit,
+ maxLines: Int
) {
SelectionContainer(modifier) {
- BasicText(text, style = style, onTextLayout = onTextLayout)
+ BasicText(text, style = style, onTextLayout = onTextLayout, maxLines = maxLines)
}
}
+
+ @RequiresDevice // b/264702195
+ @Test
+ fun magnifier_goesToLastLine_whenSelectionEndDraggedBelowTextBounds_whenTextOverflowed() {
+ checkMagnifierAsHandleGoesOutOfBoundsUsingMaxLines(Handle.SelectionEnd)
+ }
+
+ @RequiresDevice // b/264702195
+ @Test
+ fun magnifier_hidden_whenSelectionStartDraggedBelowTextBounds_whenTextOverflowed() {
+ checkMagnifierAsHandleGoesOutOfBoundsUsingMaxLines(Handle.SelectionStart)
+ }
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
index 03d1910..76317a7 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
@@ -85,7 +85,6 @@
import java.util.concurrent.CountDownLatch
import kotlin.math.max
import kotlin.math.sign
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -333,7 +332,7 @@
}
@Test
- fun selectionHandle_remainsInComposition_whenTextIsOverflown_clipped_softwrapDisabled() {
+ fun selectionHandle_remainsInComposition_whenTextIsOverflowed_clipped_softwrapDisabled() {
createSelectionContainer {
Column {
BasicText(
@@ -370,35 +369,53 @@
assertAnchorInfo(selection.value?.end, offset = 4, selectableId = 2)
}
- @Ignore("b/262428141")
@Test
- fun selectionHandle_remainsInComposition_whenTextIsOverflown_clipped_maxLines1() {
+ fun allTextIsSelected_whenTextIsOverflowed_clipped_maxLines1() = with(rule.density) {
+ val longText = "$textContent ".repeat(100)
createSelectionContainer {
Column {
BasicText(
- AnnotatedString("$textContent ".repeat(100)),
- Modifier
- .fillMaxWidth()
- .testTag(tag1),
+ AnnotatedString(longText),
+ Modifier.fillMaxWidth().testTag(tag1),
style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
maxLines = 1
)
- DisableSelection {
- BasicText(
- textContent,
- Modifier.fillMaxWidth().testTag(tag2),
- style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
- maxLines = 1
- )
- }
}
}
startSelection(tag1)
- dragHandleTo(Handle.SelectionEnd, offset = characterBox(tag2, 4).center)
+ dragHandleTo(
+ handle = Handle.SelectionEnd,
+ offset = characterBox(tag1, 4).bottomRight + Offset(x = 0f, y = fontSize.toPx())
+ )
assertAnchorInfo(selection.value?.start, offset = 0, selectableId = 1)
- assertAnchorInfo(selection.value?.end, offset = 4, selectableId = 2)
+ assertAnchorInfo(selection.value?.end, offset = longText.length, selectableId = 1)
+ }
+
+ @Test
+ fun allTextIsSelected_whenTextIsOverflowed_ellipsized_maxLines1() = with(rule.density) {
+ val longText = "$textContent ".repeat(100)
+ createSelectionContainer {
+ Column {
+ BasicText(
+ AnnotatedString(longText),
+ Modifier.fillMaxWidth().testTag(tag1),
+ style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+
+ startSelection(tag1)
+ dragHandleTo(
+ handle = Handle.SelectionEnd,
+ offset = characterBox(tag1, 4).bottomRight + Offset(x = 0f, y = fontSize.toPx())
+ )
+
+ assertAnchorInfo(selection.value?.start, offset = 0, selectableId = 1)
+ assertAnchorInfo(selection.value?.end, offset = longText.length, selectableId = 1)
}
@Test
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldMagnifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldMagnifierTest.kt
index 75614836..0cc0ff8 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldMagnifierTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldMagnifierTest.kt
@@ -56,16 +56,18 @@
text: String,
modifier: Modifier,
style: TextStyle,
- onTextLayout: (TextLayoutResult) -> Unit
+ onTextLayout: (TextLayoutResult) -> Unit,
+ maxLines: Int
) {
BasicTextField(
text,
- onValueChange = {},
- modifier = modifier,
- textStyle = style,
- onTextLayout = onTextLayout
- )
- }
+ onValueChange = {},
+ modifier = modifier,
+ textStyle = style,
+ onTextLayout = onTextLayout,
+ maxLines = Int.MAX_VALUE
+ )
+ }
@Test
fun magnifier_appears_whileStartCursorTouched() {
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt
index b5e0699..0df1c34 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt
@@ -182,17 +182,27 @@
var needsInvalidation = false
if (source == NestedScrollSource.Drag) {
- if (leftForOverscroll.x > 0) {
+ // Ignore small deltas (< 0.5) as this usually comes from floating point rounding issues
+ // and can cause scrolling to lock up (b/265363356)
+ val appliedHorizontalOverscroll = if (leftForOverscroll.x > 0.5f) {
pullLeft(leftForOverscroll, pointer)
- } else if (leftForOverscroll.x < 0) {
+ true
+ } else if (leftForOverscroll.x < -0.5f) {
pullRight(leftForOverscroll, pointer)
+ true
+ } else {
+ false
}
- if (leftForOverscroll.y > 0) {
+ val appliedVerticalOverscroll = if (leftForOverscroll.y > 0.5f) {
pullTop(leftForOverscroll, pointer)
- } else if (leftForOverscroll.y < 0) {
+ true
+ } else if (leftForOverscroll.y < -0.5f) {
pullBottom(leftForOverscroll, pointer)
+ true
+ } else {
+ false
}
- needsInvalidation = leftForOverscroll != Offset.Zero
+ needsInvalidation = appliedHorizontalOverscroll || appliedVerticalOverscroll
}
needsInvalidation = releaseOppositeOverscroll(delta) || needsInvalidation
if (needsInvalidation) invalidateOverscroll()
@@ -463,28 +473,62 @@
private fun pullTop(scroll: Offset, displacement: Offset): Float {
val displacementX: Float = displacement.x / containerSize.width
val pullY = scroll.y / containerSize.height
- return topEffect.onPullDistanceCompat(pullY, displacementX) * containerSize.height
+ val consumed = topEffect.onPullDistanceCompat(pullY, displacementX) * containerSize.height
+ // If overscroll is showing, assume we have consumed all the provided scroll, and return
+ // that amount directly to avoid floating point rounding issues (b/265363356)
+ return if (topEffect.distanceCompat != 0f) {
+ scroll.y
+ } else {
+ consumed
+ }
}
private fun pullBottom(scroll: Offset, displacement: Offset): Float {
val displacementX: Float = displacement.x / containerSize.width
val pullY = scroll.y / containerSize.height
- return -bottomEffect.onPullDistanceCompat(
+ val consumed = -bottomEffect.onPullDistanceCompat(
-pullY,
1 - displacementX
) * containerSize.height
+ // If overscroll is showing, assume we have consumed all the provided scroll, and return
+ // that amount directly to avoid floating point rounding issues (b/265363356)
+ return if (bottomEffect.distanceCompat != 0f) {
+ scroll.y
+ } else {
+ consumed
+ }
}
private fun pullLeft(scroll: Offset, displacement: Offset): Float {
val displacementY: Float = displacement.y / containerSize.height
val pullX = scroll.x / containerSize.width
- return leftEffect.onPullDistanceCompat(pullX, 1 - displacementY) * containerSize.width
+ val consumed = leftEffect.onPullDistanceCompat(
+ pullX,
+ 1 - displacementY
+ ) * containerSize.width
+ // If overscroll is showing, assume we have consumed all the provided scroll, and return
+ // that amount directly to avoid floating point rounding issues (b/265363356)
+ return if (leftEffect.distanceCompat != 0f) {
+ scroll.x
+ } else {
+ consumed
+ }
}
private fun pullRight(scroll: Offset, displacement: Offset): Float {
val displacementY: Float = displacement.y / containerSize.height
val pullX = scroll.x / containerSize.width
- return -rightEffect.onPullDistanceCompat(-pullX, displacementY) * containerSize.width
+ val consumed = -rightEffect.onPullDistanceCompat(
+ -pullX,
+ displacementY
+ ) * containerSize.width
+ // If overscroll is showing, assume we have consumed all the provided scroll, and return
+ // that amount directly to avoid floating point rounding issues (b/265363356)
+ return if (rightEffect.distanceCompat != 0f) {
+ scroll.x
+ } else {
+ consumed
+ }
}
}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/SystemGestureExclusion.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/SystemGestureExclusion.kt
index df702a8..a62fdb5 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/SystemGestureExclusion.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/SystemGestureExclusion.kt
@@ -26,7 +26,6 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.graphics.toAndroidRect
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.OnGloballyPositionedModifier
import androidx.compose.ui.layout.boundsInRoot
@@ -97,7 +96,13 @@
override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
val newRect = if (exclusion == null) {
- coordinates.boundsInRoot().toAndroidRect()
+ val boundsInRoot = coordinates.boundsInRoot()
+ android.graphics.Rect(
+ boundsInRoot.left.roundToInt(),
+ boundsInRoot.top.roundToInt(),
+ boundsInRoot.right.roundToInt(),
+ boundsInRoot.bottom.roundToInt()
+ )
} else {
calcBounds(coordinates, exclusion.invoke(coordinates))
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Overscroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Overscroll.kt
index c8c89557..927a72e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Overscroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Overscroll.kt
@@ -53,6 +53,19 @@
* sum of all the delta that was consumed during this operation - both by the overscroll and
* [performScroll].
*
+ * For example, assume we want to apply overscroll to a custom component that isn't using
+ * [androidx.compose.foundation.gestures.scrollable]. Here is a simple example of a component
+ * using [androidx.compose.foundation.gestures.draggable] instead:
+ *
+ * @sample androidx.compose.foundation.samples.OverscrollWithDraggable_Before
+ *
+ * To apply overscroll, we need to decorate the existing logic with applyToScroll, and
+ * return the amount of delta we have consumed when updating the drag position. Note that we
+ * also need to call applyToFling - this is used as an end signal for overscroll so that effects
+ * can correctly reset after any animations, when the gesture has stopped.
+ *
+ * @sample androidx.compose.foundation.samples.OverscrollWithDraggable_After
+ *
* @param delta total scroll delta available
* @param source the source of the delta
* @param performScroll the scroll action that the overscroll is applied to. The [Offset]
@@ -75,6 +88,18 @@
* as to release any existing tension. The implementation *must* call [performFling] exactly
* once.
*
+ * For example, assume we want to apply overscroll to a custom component that isn't using
+ * [androidx.compose.foundation.gestures.scrollable]. Here is a simple example of a component
+ * using [androidx.compose.foundation.gestures.draggable] instead:
+ *
+ * @sample androidx.compose.foundation.samples.OverscrollWithDraggable_Before
+ *
+ * To apply overscroll, we decorate the existing logic with applyToScroll, and return the amount
+ * of delta we have consumed when updating the drag position. We then call applyToFling using
+ * the velocity provided by onDragStopped.
+ *
+ * @sample androidx.compose.foundation.samples.OverscrollWithDraggable_After
+ *
* @param velocity total [Velocity] available
* @param performFling the [Velocity] consuming lambda that the overscroll is applied to. The
* [Velocity] parameter represents how much [Velocity] is available, and the return value is how
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
index 28f4c56..cd39f68 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
@@ -36,6 +36,7 @@
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerHoverIcon
@@ -437,22 +438,32 @@
state.layoutResult?.let {
state.drawScopeInvalidation
val selection = selectionRegistrar?.subselections?.get(state.selectableId)
+ val lastVisibleOffset = state.selectable?.getLastVisibleOffset() ?: 0
if (selection != null) {
val start = if (!selection.handlesCrossed) {
selection.start.offset
} else {
selection.end.offset
- }
+ }.coerceIn(0, lastVisibleOffset)
+ // selection path should end at the last visible character.
val end = if (!selection.handlesCrossed) {
selection.end.offset
} else {
selection.start.offset
- }
+ }.coerceIn(0, lastVisibleOffset)
if (start != end) {
val selectionPath = it.multiParagraph.getPathForRange(start, end)
- drawPath(selectionPath, state.selectionBackgroundColor)
+ // clip selection path drawing so that it doesn't overflow, unless
+ // overflow is also TextOverflow.Visible
+ if (it.layoutInput.overflow == TextOverflow.Visible) {
+ drawPath(selectionPath, state.selectionBackgroundColor)
+ } else {
+ clipRect {
+ drawPath(selectionPath, state.selectionBackgroundColor)
+ }
+ }
}
}
drawIntoCanvas { canvas ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
index 4099d638..4d59f31 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
@@ -30,6 +30,40 @@
private val layoutResultCallback: () -> TextLayoutResult?
) : Selectable {
+ private var _previousTextLayoutResult: TextLayoutResult? = null
+
+ // previously calculated `lastVisibleOffset` for the `_previousTextLayoutResult`
+ private var _previousLastVisibleOffset: Int = -1
+
+ /**
+ * TextLayoutResult is not expected to change repeatedly in a BasicText composable. At least
+ * most TextLayoutResult changes would likely affect Selection logic in some way. Therefore,
+ * this value only caches the last visible offset calculation for the latest seen
+ * TextLayoutResult instance. Object equality check is not worth the extra calculation as
+ * instance check is enough to accomplish whether a text layout has changed in a meaningful
+ * way.
+ */
+ private val TextLayoutResult.lastVisibleOffset: Int
+ @Synchronized get() {
+ if (_previousTextLayoutResult !== this) {
+ val lastVisibleLine = when {
+ !didOverflowHeight || multiParagraph.didExceedMaxLines -> lineCount - 1
+ else -> { // size.height < multiParagraph.height
+ var finalVisibleLine = getLineForVerticalPosition(size.height.toFloat())
+ .coerceAtMost(lineCount - 1)
+ // if final visible line's top is equal to or larger than text layout
+ // result's height, we need to check above lines one by one until we find
+ // a line that fits in boundaries.
+ while (getLineTop(finalVisibleLine) >= size.height) finalVisibleLine--
+ finalVisibleLine
+ }
+ }
+ _previousLastVisibleOffset = getLineEnd(lastVisibleLine, true)
+ _previousTextLayoutResult = this
+ }
+ return _previousLastVisibleOffset
+ }
+
override fun updateSelection(
startHandlePosition: Offset,
endHandlePosition: Offset,
@@ -92,9 +126,11 @@
if (getLayoutCoordinates() == null) return Offset.Zero
val textLayoutResult = layoutResultCallback() ?: return Offset.Zero
+ val offset = if (isStartHandle) selection.start.offset else selection.end.offset
+ val coercedOffset = offset.coerceIn(0, textLayoutResult.lastVisibleOffset)
return getSelectionHandleCoordinates(
textLayoutResult = textLayoutResult,
- offset = if (isStartHandle) selection.start.offset else selection.end.offset,
+ offset = coercedOffset,
isStart = isStartHandle,
areHandlesCrossed = selection.handlesCrossed
)
@@ -122,14 +158,19 @@
override fun getRangeOfLineContaining(offset: Int): TextRange {
val textLayoutResult = layoutResultCallback() ?: return TextRange.Zero
- val textLength = textLayoutResult.layoutInput.text.length
- if (textLength < 1) return TextRange.Zero
- val line = textLayoutResult.getLineForOffset(offset.coerceIn(0, textLength - 1))
+ val visibleTextLength = textLayoutResult.lastVisibleOffset
+ if (visibleTextLength < 1) return TextRange.Zero
+ val line = textLayoutResult.getLineForOffset(offset.coerceIn(0, visibleTextLength - 1))
return TextRange(
start = textLayoutResult.getLineStart(line),
end = textLayoutResult.getLineEnd(line, visibleEnd = true)
)
}
+
+ override fun getLastVisibleOffset(): Int {
+ val textLayoutResult = layoutResultCallback() ?: return 0
+ return textLayoutResult.lastVisibleOffset
+ }
}
/**
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/Selectable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/Selectable.kt
index 5206418..0d227f8 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/Selectable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/Selectable.kt
@@ -20,7 +20,10 @@
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.MultiParagraph
+import androidx.compose.ui.text.TextLayoutInput
import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.unit.Constraints
/**
* Provides [Selection] information for a composable to SelectionContainer. Composables who can
@@ -117,7 +120,17 @@
* Return the offsets of the start and end of the line containing [offset], or [TextRange.Zero]
* if the selectable is empty. These offsets are in the same "coordinate space" as
* [getBoundingBox], and despite being returned in a [TextRange], may not refer to offsets in
- * actual text if the selectable contains other types of content.
+ * actual text if the selectable contains other types of content. This function returns
+ * the last visible line's boundaries if offset is larger than text length or the character at
+ * given offset would fall on a line which is hidden by maxLines or Constraints.
*/
fun getRangeOfLineContaining(offset: Int): TextRange
+
+ /**
+ * Returns the last visible character's offset. Some lines can be hidden due to either
+ * [TextLayoutInput.maxLines] or [Constraints.maxHeight] being smaller than
+ * [MultiParagraph.height]. If overflow is set to clip and a line is partially visible, it
+ * counts as the last visible line.
+ */
+ fun getLastVisibleOffset(): Int
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
index e249b73..7e1069b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
@@ -807,6 +807,8 @@
// The end offset is exclusive.
val offset = if (isStartHandle) anchor.offset else anchor.offset - 1
+ if (offset > selectable.getLastVisibleOffset()) return Offset.Unspecified
+
// The horizontal position doesn't snap to cursor positions but should directly track the
// actual drag.
val localDragPosition = selectableCoordinates.localPositionOf(
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt
index 6f49d4c..034e5cd 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt
@@ -223,6 +223,7 @@
var getTextCalledTimes = 0
var selectionToReturn: Selection? = null
var textToReturn: AnnotatedString? = null
+ var lastVisibleOffsetToReturn: Int = 0
var handlePosition = Offset.Zero
var boundingBox = Rect.Zero
@@ -287,6 +288,10 @@
return TextRange.Zero
}
+ override fun getLastVisibleOffset(): Int {
+ return lastVisibleOffsetToReturn
+ }
+
fun clear() {
lastEndHandlePosition = null
lastStartHandlePosition = null
@@ -299,5 +304,6 @@
getTextCalledTimes = 0
selectionToReturn = null
textToReturn = null
+ lastVisibleOffsetToReturn = 0
}
}
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/PagerActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/PagerActivity.kt
index 3ffe581..a9466ca 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/PagerActivity.kt
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/PagerActivity.kt
@@ -23,8 +23,9 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PageSize
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@@ -44,12 +45,18 @@
setContent {
val pagerState = rememberPagerState()
- Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
HorizontalPager(
modifier = Modifier
+ .height(400.dp)
.semantics { contentDescription = "Pager" }
.background(Color.White),
state = pagerState,
+ pageSize = PageSize.Fill,
pageCount = itemCount
) {
PagerItem(it)
@@ -69,9 +76,8 @@
private fun PagerItem(index: Int) {
Box(
modifier = Modifier
- .size(200.dp, 400.dp)
- .background(Color.Black),
- contentAlignment = Alignment.Center
+ .fillMaxSize()
+ .background(Color.Black)
) {
Text(text = index.toString(), color = Color.White)
}
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/ViewPagerActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/ViewPagerActivity.kt
index b1be881..01ac67f 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/ViewPagerActivity.kt
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/ViewPagerActivity.kt
@@ -22,20 +22,16 @@
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
+import androidx.viewpager2.widget.ViewPager2
class ViewPagerActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_pager)
- val pager = findViewById<RecyclerView>(R.id.pager)
- pager.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
+ val pager = findViewById<ViewPager2>(R.id.pager)
val itemCount = intent.getIntExtra(ExtraItemCount, 3000)
val adapter = PagerAdapter(itemCount)
- val scroller = PagerSnapHelper()
- scroller.attachToRecyclerView(pager)
pager.adapter = adapter
launchIdlenessTracking()
}
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/layout/activity_view_pager.xml b/compose/integration-tests/macrobenchmark-target/src/main/res/layout/activity_view_pager.xml
index 1ce3f46..50631f8 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/res/layout/activity_view_pager.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/layout/activity_view_pager.xml
@@ -20,14 +20,14 @@
android:layout_gravity="center"
android:gravity="center"
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="400dp">
- <androidx.recyclerview.widget.RecyclerView
+ <androidx.viewpager2.widget.ViewPager2
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pager"
android:layout_gravity="center"
android:gravity="center"
android:orientation="horizontal"
android:layout_width="match_parent"
- android:layout_height="wrap_content" />
+ android:layout_height="match_parent" />
</FrameLayout>
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/layout/view_pager_item.xml b/compose/integration-tests/macrobenchmark-target/src/main/res/layout/view_pager_item.xml
index 33e2e5c..69067a4 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/res/layout/view_pager_item.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/layout/view_pager_item.xml
@@ -15,9 +15,9 @@
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="200dp"
+ android:layout_width="match_parent"
android:background="#000000"
- android:layout_height="400dp"
+ android:layout_height="match_parent"
android:layout_gravity="center">
<TextView
diff --git a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt
index ae926e8..c969613 100644
--- a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt
+++ b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt
@@ -29,9 +29,7 @@
import androidx.annotation.RequiresApi
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.toAndroidRect
import androidx.compose.ui.graphics.toArgb
import java.lang.reflect.Method
@@ -179,7 +177,12 @@
// another invalidation, etc.
ripple.trySetRadius(radius)
ripple.setColor(color, alpha)
- val newBounds = size.toRect().toAndroidRect()
+ val newBounds = Rect(
+ 0,
+ 0,
+ size.width.toInt(),
+ size.height.toInt()
+ )
// Drawing the background causes the view to update the bounds of the drawable
// based on the view's bounds, so we need to adjust the view itself to match the
// canvas' bounds.
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index e3bd3b6..b56b591 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -232,6 +232,9 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
}
+ public final class DateInputKt {
+ }
+
public final class DatePickerDialog_androidKt {
}
@@ -503,7 +506,7 @@
method public int getCircularDeterminateStrokeCap();
method public int getCircularIndeterminateStrokeCap();
method public float getCircularStrokeWidth();
- method public long getCircularTrackColor();
+ method @androidx.compose.runtime.Composable public long getCircularTrackColor();
method @androidx.compose.runtime.Composable public long getLinearColor();
method public int getLinearStrokeCap();
method @androidx.compose.runtime.Composable public long getLinearTrackColor();
@@ -514,7 +517,7 @@
property public final int LinearStrokeCap;
property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
property @androidx.compose.runtime.Composable public final long circularColor;
- property public final long circularTrackColor;
+ property @androidx.compose.runtime.Composable public final long circularTrackColor;
property @androidx.compose.runtime.Composable public final long linearColor;
property @androidx.compose.runtime.Composable public final long linearTrackColor;
field public static final androidx.compose.material3.ProgressIndicatorDefaults INSTANCE;
@@ -601,12 +604,24 @@
}
@androidx.compose.runtime.Stable public final class SliderDefaults {
+ method @androidx.compose.runtime.Composable public void Thumb(androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional long thumbSize);
+ method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
method @androidx.compose.runtime.Composable public androidx.compose.material3.SliderColors colors(optional long thumbColor, optional long activeTrackColor, optional long activeTickColor, optional long inactiveTrackColor, optional long inactiveTickColor, optional long disabledThumbColor, optional long disabledActiveTrackColor, optional long disabledActiveTickColor, optional long disabledInactiveTrackColor, optional long disabledInactiveTickColor);
field public static final androidx.compose.material3.SliderDefaults INSTANCE;
}
public final class SliderKt {
- method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+ method @androidx.compose.runtime.Composable public static void RangeSlider(kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> value, kotlin.jvm.functions.Function1<? super kotlin.ranges.ClosedFloatingPointRange<java.lang.Float>,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource startInteractionSource, optional androidx.compose.foundation.interaction.MutableInteractionSource endInteractionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> startThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> endThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> track, optional int steps);
+ method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> thumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> track, optional int steps);
+ method @Deprecated @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+ }
+
+ @androidx.compose.runtime.Stable public final class SliderPositions {
+ ctor public SliderPositions(optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> initialActiveRange, optional float[] initialTickFractions);
+ method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getActiveRange();
+ method public float[] getTickFractions();
+ property public final kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> activeRange;
+ property public final float[] tickFractions;
}
@androidx.compose.runtime.Stable public interface SnackbarData {
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 45313fd..ff21164 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -281,10 +281,20 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
}
+ @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class DateInputDefaults {
+ method @androidx.compose.runtime.Composable public void DateInputHeadline(androidx.compose.material3.DatePickerState state, androidx.compose.material3.DatePickerFormatter dateFormatter);
+ method @androidx.compose.runtime.Composable public void DateInputTitle();
+ field public static final androidx.compose.material3.DateInputDefaults INSTANCE;
+ }
+
+ public final class DateInputKt {
+ method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void DateInput(androidx.compose.material3.DatePickerState dateInputState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.DatePickerFormatter dateFormatter, optional kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> dateValidator, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit> headline, optional androidx.compose.material3.DatePickerColors colors);
+ }
+
@androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class DatePickerColors {
}
- @androidx.compose.material3.ExperimentalMaterial3Api public final class DatePickerDefaults {
+ @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class DatePickerDefaults {
method @androidx.compose.runtime.Composable public void DatePickerHeadline(androidx.compose.material3.DatePickerState state, androidx.compose.material3.DatePickerFormatter dateFormatter);
method @androidx.compose.runtime.Composable public void DatePickerTitle();
method @androidx.compose.runtime.Composable public androidx.compose.material3.DatePickerColors colors(optional long containerColor, optional long titleContentColor, optional long headlineContentColor, optional long weekdayContentColor, optional long subheadContentColor, optional long yearContentColor, optional long currentYearContentColor, optional long selectedYearContentColor, optional long selectedYearContainerColor, optional long dayContentColor, optional long disabledDayContentColor, optional long selectedDayContentColor, optional long disabledSelectedDayContentColor, optional long selectedDayContainerColor, optional long disabledSelectedDayContainerColor, optional long todayContentColor, optional long todayDateBorderColor);
@@ -711,7 +721,7 @@
method public int getCircularDeterminateStrokeCap();
method public int getCircularIndeterminateStrokeCap();
method public float getCircularStrokeWidth();
- method public long getCircularTrackColor();
+ method @androidx.compose.runtime.Composable public long getCircularTrackColor();
method @androidx.compose.runtime.Composable public long getLinearColor();
method public int getLinearStrokeCap();
method @androidx.compose.runtime.Composable public long getLinearTrackColor();
@@ -722,7 +732,7 @@
property public final int LinearStrokeCap;
property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
property @androidx.compose.runtime.Composable public final long circularColor;
- property public final long circularTrackColor;
+ property @androidx.compose.runtime.Composable public final long circularTrackColor;
property @androidx.compose.runtime.Composable public final long linearColor;
property @androidx.compose.runtime.Composable public final long linearTrackColor;
field public static final androidx.compose.material3.ProgressIndicatorDefaults INSTANCE;
@@ -838,20 +848,22 @@
}
@androidx.compose.runtime.Stable public final class SliderDefaults {
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Thumb(androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional long thumbSize);
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
+ method @androidx.compose.runtime.Composable public void Thumb(androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional long thumbSize);
+ method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
method @androidx.compose.runtime.Composable public androidx.compose.material3.SliderColors colors(optional long thumbColor, optional long activeTrackColor, optional long activeTickColor, optional long inactiveTrackColor, optional long inactiveTickColor, optional long disabledThumbColor, optional long disabledActiveTrackColor, optional long disabledActiveTickColor, optional long disabledInactiveTrackColor, optional long disabledInactiveTickColor);
field public static final androidx.compose.material3.SliderDefaults INSTANCE;
}
public final class SliderKt {
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RangeSlider(kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> value, kotlin.jvm.functions.Function1<? super kotlin.ranges.ClosedFloatingPointRange<java.lang.Float>,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource startInteractionSource, optional androidx.compose.foundation.interaction.MutableInteractionSource endInteractionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> startThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> endThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> track, optional int steps);
- method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> thumb);
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> track, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> thumb);
+ method @androidx.compose.runtime.Composable public static void RangeSlider(kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> value, kotlin.jvm.functions.Function1<? super kotlin.ranges.ClosedFloatingPointRange<java.lang.Float>,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource startInteractionSource, optional androidx.compose.foundation.interaction.MutableInteractionSource endInteractionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> startThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> endThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> track, optional int steps);
+ method @Deprecated @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RangeSlider(kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> value, kotlin.jvm.functions.Function1<? super kotlin.ranges.ClosedFloatingPointRange<java.lang.Float>,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors);
+ method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> thumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> track, optional int steps);
+ method @Deprecated @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+ method @Deprecated @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,? extends kotlin.Unit> thumb);
+ method @Deprecated @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends kotlin.Unit> onValueChange, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,? extends kotlin.Unit> track, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,? extends kotlin.Unit> thumb);
}
- @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SliderPositions {
+ @androidx.compose.runtime.Stable public final class SliderPositions {
ctor public SliderPositions(optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> initialActiveRange, optional float[] initialTickFractions);
method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getActiveRange();
method public float[] getTickFractions();
@@ -1069,6 +1081,10 @@
public final class TonalPaletteKt {
}
+ @androidx.compose.material3.ExperimentalMaterial3Api public interface TooltipBoxScope {
+ method public androidx.compose.ui.Modifier tooltipAnchor(androidx.compose.ui.Modifier);
+ }
+
@androidx.compose.material3.ExperimentalMaterial3Api public final class TooltipDefaults {
method @androidx.compose.runtime.Composable public long getPlainTooltipContainerColor();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getPlainTooltipContainerShape();
@@ -1080,7 +1096,7 @@
}
public final class TooltipKt {
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltipBox(kotlin.jvm.functions.Function0<kotlin.Unit> tooltip, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.TooltipState? tooltipState, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltipBox(kotlin.jvm.functions.Function0<kotlin.Unit> tooltip, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.TooltipState tooltipState, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.material3.TooltipBoxScope,kotlin.Unit> content);
}
@androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class TooltipState {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index e3bd3b6..b56b591 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -232,6 +232,9 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
}
+ public final class DateInputKt {
+ }
+
public final class DatePickerDialog_androidKt {
}
@@ -503,7 +506,7 @@
method public int getCircularDeterminateStrokeCap();
method public int getCircularIndeterminateStrokeCap();
method public float getCircularStrokeWidth();
- method public long getCircularTrackColor();
+ method @androidx.compose.runtime.Composable public long getCircularTrackColor();
method @androidx.compose.runtime.Composable public long getLinearColor();
method public int getLinearStrokeCap();
method @androidx.compose.runtime.Composable public long getLinearTrackColor();
@@ -514,7 +517,7 @@
property public final int LinearStrokeCap;
property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
property @androidx.compose.runtime.Composable public final long circularColor;
- property public final long circularTrackColor;
+ property @androidx.compose.runtime.Composable public final long circularTrackColor;
property @androidx.compose.runtime.Composable public final long linearColor;
property @androidx.compose.runtime.Composable public final long linearTrackColor;
field public static final androidx.compose.material3.ProgressIndicatorDefaults INSTANCE;
@@ -601,12 +604,24 @@
}
@androidx.compose.runtime.Stable public final class SliderDefaults {
+ method @androidx.compose.runtime.Composable public void Thumb(androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled, optional long thumbSize);
+ method @androidx.compose.runtime.Composable public void Track(androidx.compose.material3.SliderPositions sliderPositions, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SliderColors colors, optional boolean enabled);
method @androidx.compose.runtime.Composable public androidx.compose.material3.SliderColors colors(optional long thumbColor, optional long activeTrackColor, optional long activeTickColor, optional long inactiveTrackColor, optional long inactiveTickColor, optional long disabledThumbColor, optional long disabledActiveTrackColor, optional long disabledActiveTickColor, optional long disabledInactiveTrackColor, optional long disabledInactiveTickColor);
field public static final androidx.compose.material3.SliderDefaults INSTANCE;
}
public final class SliderKt {
- method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+ method @androidx.compose.runtime.Composable public static void RangeSlider(kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> value, kotlin.jvm.functions.Function1<? super kotlin.ranges.ClosedFloatingPointRange<java.lang.Float>,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource startInteractionSource, optional androidx.compose.foundation.interaction.MutableInteractionSource endInteractionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> startThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> endThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> track, optional int steps);
+ method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> thumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderPositions,kotlin.Unit> track, optional int steps);
+ method @Deprecated @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+ }
+
+ @androidx.compose.runtime.Stable public final class SliderPositions {
+ ctor public SliderPositions(optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> initialActiveRange, optional float[] initialTickFractions);
+ method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getActiveRange();
+ method public float[] getTickFractions();
+ property public final kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> activeRange;
+ property public final float[] tickFractions;
}
@androidx.compose.runtime.Stable public interface SnackbarData {
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 33ddf18..94fceef 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -38,6 +38,8 @@
import androidx.compose.material3.samples.ClickableCardSample
import androidx.compose.material3.samples.ClickableElevatedCardSample
import androidx.compose.material3.samples.ClickableOutlinedCardSample
+import androidx.compose.material3.samples.DateInputDialogSample
+import androidx.compose.material3.samples.DateInputSample
import androidx.compose.material3.samples.DatePickerDialogSample
import androidx.compose.material3.samples.DatePickerSample
import androidx.compose.material3.samples.DatePickerWithDateValidatorSample
@@ -363,6 +365,20 @@
) {
DatePickerWithDateValidatorSample()
},
+ Example(
+ name = ::DateInputSample.name,
+ description = DatePickerExampleDescription,
+ sourceUrl = DatePickerExampleSourceUrl
+ ) {
+ DateInputSample()
+ },
+ Example(
+ name = ::DateInputDialogSample.name,
+ description = DatePickerExampleDescription,
+ sourceUrl = DatePickerExampleSourceUrl
+ ) {
+ DateInputDialogSample()
+ },
)
private const val DialogExampleDescription = "Dialog examples"
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DatePickerSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DatePickerSamples.kt
index e067984..b1f9b9c 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DatePickerSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DatePickerSamples.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.DateInput
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -49,9 +50,10 @@
@Composable
fun DatePickerSample() {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
- // Pre-select a date with January 4, 2020
+ // Pre-select a date for January 4, 2020
val datePickerState = rememberDatePickerState(initialSelectedDateMillis = 1578096000000)
DatePicker(datePickerState = datePickerState, modifier = Modifier.padding(16.dp))
+
Text("Selected date timestamp: ${datePickerState.selectedDateMillis ?: "no selection"}")
}
}
@@ -134,3 +136,68 @@
Text("Selected date timestamp: ${datePickerState.selectedDateMillis ?: "no selection"}")
}
}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Sampled
+@Composable
+fun DateInputSample() {
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ val dateInputState = rememberDatePickerState()
+ DateInput(dateInputState = dateInputState, modifier = Modifier.padding(16.dp))
+
+ Text("Entered date timestamp: ${dateInputState.selectedDateMillis ?: "no input"}")
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Sampled
+@Composable
+fun DateInputDialogSample() {
+ // Decoupled snackbar host state from scaffold state for demo purposes.
+ val snackState = remember { SnackbarHostState() }
+ val snackScope = rememberCoroutineScope()
+ SnackbarHost(hostState = snackState, Modifier)
+ val openDialog = remember { mutableStateOf(true) }
+ // TODO demo how to read the selected date from the state.
+ if (openDialog.value) {
+ // Pre-select a date for January 4, 2020
+ val dateInputState = rememberDatePickerState(initialSelectedDateMillis = 1578096000000)
+ val confirmEnabled = derivedStateOf { dateInputState.selectedDateMillis != null }
+ DatePickerDialog(
+ onDismissRequest = {
+ // Dismiss the dialog when the user clicks outside the dialog or on the back
+ // button. If you want to disable that functionality, simply use an empty
+ // onDismissRequest.
+ openDialog.value = false
+ },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ openDialog.value = false
+ snackScope.launch {
+ snackState.showSnackbar(
+ "Entered date timestamp: ${dateInputState.selectedDateMillis}"
+ )
+ }
+ },
+ enabled = confirmEnabled.value
+ ) {
+ Text("OK")
+ }
+ },
+ dismissButton = {
+ TextButton(
+ onClick = {
+ openDialog.value = false
+ }
+ ) {
+ Text("Cancel")
+ }
+ }
+ ) {
+ DateInput(dateInputState = dateInputState)
+ }
+ }
+}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TooltipSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TooltipSamples.kt
index a8426f0..0e0f598 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TooltipSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TooltipSamples.kt
@@ -44,15 +44,18 @@
@Sampled
@Composable
fun PlainTooltipSample() {
+ val tooltipState = remember { TooltipState() }
PlainTooltipBox(
- tooltip = { Text("Add to favorites") }
+ tooltip = { Text("Add to favorites") },
+ tooltipState = tooltipState
) {
IconButton(
- onClick = { /* Icon button's click event */ }
+ onClick = { /* Icon button's click event */ },
+ modifier = Modifier.tooltipAnchor()
) {
Icon(
imageVector = Icons.Filled.Favorite,
- contentDescription = null
+ contentDescription = "Localized Description"
)
}
}
@@ -65,7 +68,6 @@
fun PlainTooltipWithManualInvocationSample() {
val tooltipState = remember { TooltipState() }
val scope = rememberCoroutineScope()
-
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
@@ -75,7 +77,7 @@
) {
Icon(
imageVector = Icons.Filled.AddCircle,
- contentDescription = null
+ contentDescription = "Localized Description"
)
}
Spacer(Modifier.requiredHeight(30.dp))
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CalendarModelTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CalendarModelTest.kt
index dbe3499..d6e6ffd 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CalendarModelTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CalendarModelTest.kt
@@ -144,29 +144,29 @@
@Test
fun dateInputFormat() {
Locale.setDefault(Locale.US)
- assertThat(model.dateInputFormat.patternWithDelimiters).isEqualTo("MM/dd/yyyy")
- assertThat(model.dateInputFormat.patternWithoutDelimiters).isEqualTo("MMddyyyy")
- assertThat(model.dateInputFormat.delimiter).isEqualTo('/')
+ assertThat(model.getDateInputFormat().patternWithDelimiters).isEqualTo("MM/dd/yyyy")
+ assertThat(model.getDateInputFormat().patternWithoutDelimiters).isEqualTo("MMddyyyy")
+ assertThat(model.getDateInputFormat().delimiter).isEqualTo('/')
Locale.setDefault(Locale.CHINA)
- assertThat(model.dateInputFormat.patternWithDelimiters).isEqualTo("yyyy/MM/dd")
- assertThat(model.dateInputFormat.patternWithoutDelimiters).isEqualTo("yyyyMMdd")
- assertThat(model.dateInputFormat.delimiter).isEqualTo('/')
+ assertThat(model.getDateInputFormat().patternWithDelimiters).isEqualTo("yyyy/MM/dd")
+ assertThat(model.getDateInputFormat().patternWithoutDelimiters).isEqualTo("yyyyMMdd")
+ assertThat(model.getDateInputFormat().delimiter).isEqualTo('/')
Locale.setDefault(Locale.UK)
- assertThat(model.dateInputFormat.patternWithDelimiters).isEqualTo("dd/MM/yyyy")
- assertThat(model.dateInputFormat.patternWithoutDelimiters).isEqualTo("ddMMyyyy")
- assertThat(model.dateInputFormat.delimiter).isEqualTo('/')
+ assertThat(model.getDateInputFormat().patternWithDelimiters).isEqualTo("dd/MM/yyyy")
+ assertThat(model.getDateInputFormat().patternWithoutDelimiters).isEqualTo("ddMMyyyy")
+ assertThat(model.getDateInputFormat().delimiter).isEqualTo('/')
Locale.setDefault(Locale.KOREA)
- assertThat(model.dateInputFormat.patternWithDelimiters).isEqualTo("yyyy.MM.dd")
- assertThat(model.dateInputFormat.patternWithoutDelimiters).isEqualTo("yyyyMMdd")
- assertThat(model.dateInputFormat.delimiter).isEqualTo('.')
+ assertThat(model.getDateInputFormat().patternWithDelimiters).isEqualTo("yyyy.MM.dd")
+ assertThat(model.getDateInputFormat().patternWithoutDelimiters).isEqualTo("yyyyMMdd")
+ assertThat(model.getDateInputFormat().delimiter).isEqualTo('.')
Locale.setDefault(Locale("es", "CL"))
- assertThat(model.dateInputFormat.patternWithDelimiters).isEqualTo("dd-MM-yyyy")
- assertThat(model.dateInputFormat.patternWithoutDelimiters).isEqualTo("ddMMyyyy")
- assertThat(model.dateInputFormat.delimiter).isEqualTo('-')
+ assertThat(model.getDateInputFormat().patternWithDelimiters).isEqualTo("dd-MM-yyyy")
+ assertThat(model.getDateInputFormat().patternWithoutDelimiters).isEqualTo("ddMMyyyy")
+ assertThat(model.getDateInputFormat().delimiter).isEqualTo('-')
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@@ -184,7 +184,7 @@
assertThat(newModel.today).isEqualTo(legacyModel.today)
assertThat(month).isEqualTo(legacyMonth)
- assertThat(newModel.dateInputFormat).isEqualTo(legacyModel.dateInputFormat)
+ assertThat(newModel.getDateInputFormat()).isEqualTo(legacyModel.getDateInputFormat())
assertThat(newModel.plusMonths(month, 3)).isEqualTo(legacyModel.plusMonths(month, 3))
assertThat(date).isEqualTo(legacyDate)
assertThat(newModel.getDayOfWeek(date)).isEqualTo(legacyModel.getDayOfWeek(date))
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputScreenshotTest.kt
new file mode 100644
index 0000000..02fdffb
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputScreenshotTest.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import android.os.Build
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.isDialog
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import java.time.LocalDate
+import java.time.LocalTime
+import java.time.ZoneId
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@LargeTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@OptIn(ExperimentalMaterial3Api::class)
+class DateInputScreenshotTest(private val scheme: ColorSchemeWrapper) {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @get:Rule
+ val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
+
+ private val wrap = Modifier.wrapContentSize(Alignment.Center)
+ private val wrapperTestTag = "dateInputWrapper"
+
+ @Test
+ fun dateInput_initialState() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ DateInput(
+ dateInputState = rememberDatePickerState()
+ )
+ }
+ }
+ assertAgainstGolden("dateInput_initialState_${scheme.name}")
+ }
+
+ @Test
+ fun dateInput_withEnteredDate() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ val dayMillis = dayInUtcMilliseconds(year = 2021, month = 3, dayOfMonth = 6)
+ DateInput(
+ dateInputState = rememberDatePickerState(
+ initialSelectedDateMillis = dayMillis
+ )
+ )
+ }
+ }
+ assertAgainstGolden("dateInput_withEnteredDate_${scheme.name}")
+ }
+
+ @Test
+ fun dateInput_invalidDateInput() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ val monthInUtcMillis = dayInUtcMilliseconds(year = 2000, month = 6, dayOfMonth = 1)
+ DateInput(
+ dateInputState = rememberDatePickerState(
+ initialDisplayedMonthMillis = monthInUtcMillis
+ ),
+ dateValidator = { false }
+ )
+ }
+ }
+ assertAgainstGolden("dateInput_invalidDateInput_${scheme.name}")
+ }
+
+ @Test
+ fun dateInput_inDialog() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ val selectedDayMillis = dayInUtcMilliseconds(year = 2021, month = 3, dayOfMonth = 6)
+ DatePickerDialog(
+ onDismissRequest = { },
+ confirmButton = { TextButton(onClick = {}) { Text("OK") } },
+ dismissButton = { TextButton(onClick = {}) { Text("Cancel") } }
+ ) {
+ DateInput(
+ dateInputState = rememberDatePickerState(
+ initialSelectedDateMillis = selectedDayMillis
+ )
+ )
+ }
+ }
+ rule.onNode(isDialog())
+ .captureToImage()
+ .assertAgainstGolden(
+ rule = screenshotRule,
+ goldenIdentifier = "dateInput_inDialog_${scheme.name}"
+ )
+ }
+
+ // Returns the given date's day as milliseconds from epoch. The returned value is for the day's
+ // start on midnight.
+ private fun dayInUtcMilliseconds(year: Int, month: Int, dayOfMonth: Int): Long =
+ LocalDate.of(year, month, dayOfMonth)
+ .atTime(LocalTime.MIDNIGHT)
+ .atZone(ZoneId.of("UTC"))
+ .toInstant()
+ .toEpochMilli()
+
+ private fun assertAgainstGolden(goldenName: String) {
+ rule.onNodeWithTag(wrapperTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, goldenName)
+ }
+
+ // Provide the ColorScheme and their name parameter in a ColorSchemeWrapper.
+ // This makes sure that the default method name and the initial Scuba image generated
+ // name is as expected.
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun parameters() = arrayOf(
+ ColorSchemeWrapper("lightTheme", lightColorScheme()),
+ ColorSchemeWrapper("darkTheme", darkColorScheme()),
+ )
+ }
+
+ class ColorSchemeWrapper(val name: String, val colorScheme: ColorScheme) {
+ override fun toString(): String {
+ return name
+ }
+ }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputTest.kt
new file mode 100644
index 0000000..d79e898
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputTest.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.compose.material3
+
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher.Companion.expectValue
+import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertContentDescriptionEquals
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTextInput
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import java.util.Calendar
+import java.util.Locale
+import java.util.TimeZone
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalMaterial3Api::class)
+class DateInputTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun dateInput() {
+ lateinit var defaultHeadline: String
+ lateinit var dateInputLabel: String
+ lateinit var dateInputState: DatePickerState
+ rule.setMaterialContent(lightColorScheme()) {
+ defaultHeadline = getString(string = Strings.DateInputHeadline)
+ dateInputLabel = getString(string = Strings.DateInputLabel)
+ val monthInUtcMillis = dayInUtcMilliseconds(year = 2019, month = 1, dayOfMonth = 1)
+ dateInputState = rememberDatePickerState(
+ initialDisplayedMonthMillis = monthInUtcMillis
+ )
+ DateInput(dateInputState = dateInputState)
+ }
+
+ rule.onNodeWithText(defaultHeadline).assertExists()
+
+ // Enter a date.
+ rule.onNodeWithText(dateInputLabel).performClick().performTextInput("01272019")
+
+ rule.runOnIdle {
+ assertThat(dateInputState.selectedDateMillis).isEqualTo(
+ dayInUtcMilliseconds(
+ year = 2019,
+ month = 1,
+ dayOfMonth = 27
+ )
+ )
+ }
+
+ rule.onNodeWithText(defaultHeadline).assertDoesNotExist()
+ rule.onNodeWithText("Jan 27, 2019").assertExists()
+ }
+
+ @Test
+ fun dateInputWithInitialDate() {
+ lateinit var dateInputState: DatePickerState
+ rule.setMaterialContent(lightColorScheme()) {
+ val initialDateMillis = dayInUtcMilliseconds(year = 2010, month = 5, dayOfMonth = 11)
+ dateInputState = rememberDatePickerState(
+ initialSelectedDateMillis = initialDateMillis,
+ )
+ DateInput(dateInputState = dateInputState)
+ }
+
+ rule.onNodeWithText("05/11/2010").assertExists()
+ rule.onNodeWithText("May 11, 2010").assertExists()
+ }
+
+ @Test
+ fun inputDateNotAllowed() {
+ lateinit var dateInputLabel: String
+ lateinit var errorMessage: String
+ lateinit var dateInputState: DatePickerState
+ rule.setMaterialContent(lightColorScheme()) {
+ dateInputLabel = getString(string = Strings.DateInputLabel)
+ errorMessage = getString(string = Strings.DateInputInvalidNotAllowed)
+ dateInputState = rememberDatePickerState()
+ DateInput(dateInputState = dateInputState,
+ // All dates are invalid for the sake of this test.
+ dateValidator = { false }
+ )
+ }
+
+ rule.onNodeWithText(dateInputLabel).performClick().performTextInput("02272020")
+
+ rule.runOnIdle {
+ assertThat(dateInputState.selectedDateMillis).isNull()
+ }
+ rule.onNodeWithText("02/27/2020")
+ .assert(keyIsDefined(SemanticsProperties.Error))
+ .assert(
+ expectValue(
+ SemanticsProperties.Error,
+ errorMessage.format("Feb 27, 2020")
+ )
+ )
+ }
+
+ @Test
+ fun inputDateOutOfRange() {
+ lateinit var dateInputLabel: String
+ lateinit var errorMessage: String
+ lateinit var dateInputState: DatePickerState
+ rule.setMaterialContent(lightColorScheme()) {
+ dateInputLabel = getString(string = Strings.DateInputLabel)
+ errorMessage = getString(string = Strings.DateInputInvalidYearRange)
+ dateInputState = rememberDatePickerState(
+ // Limit the years selection to 2018-2023
+ yearRange = IntRange(2018, 2023)
+ )
+ DateInput(dateInputState = dateInputState)
+ }
+
+ rule.onNodeWithText(dateInputLabel).performClick().performTextInput("02272030")
+
+ rule.runOnIdle {
+ assertThat(dateInputState.selectedDateMillis).isNull()
+ }
+ rule.onNodeWithText("02/27/2030")
+ .assert(keyIsDefined(SemanticsProperties.Error))
+ .assert(
+ expectValue(
+ SemanticsProperties.Error,
+ errorMessage.format(
+ dateInputState.yearRange.first,
+ dateInputState.yearRange.last
+ )
+ )
+ )
+ }
+
+ @Test
+ fun inputDateInvalidForPattern() {
+ lateinit var dateInputLabel: String
+ lateinit var errorMessage: String
+ lateinit var dateInputState: DatePickerState
+ rule.setMaterialContent(lightColorScheme()) {
+ dateInputLabel = getString(string = Strings.DateInputLabel)
+ errorMessage =
+ getString(string = Strings.DateInputInvalidForPattern).format("MM/DD/YYYY")
+ dateInputState = rememberDatePickerState()
+ DateInput(dateInputState = dateInputState)
+ }
+
+ rule.onNodeWithText(dateInputLabel).performClick().performTextInput("99272030")
+
+ rule.runOnIdle {
+ assertThat(dateInputState.selectedDateMillis).isNull()
+ }
+ rule.onNodeWithText("99/27/2030")
+ .assert(keyIsDefined(SemanticsProperties.Error))
+ .assert(expectValue(SemanticsProperties.Error, errorMessage))
+ }
+
+ @Test
+ fun defaultSemantics() {
+ val selectedDateInUtcMillis = dayInUtcMilliseconds(year = 2010, month = 5, dayOfMonth = 11)
+ lateinit var expectedHeadlineStringFormat: String
+ rule.setMaterialContent(lightColorScheme()) {
+ // e.g. "Entered date: %1$s"
+ expectedHeadlineStringFormat = getString(Strings.DateInputHeadlineDescription)
+ DateInput(
+ dateInputState = rememberDatePickerState(
+ initialSelectedDateMillis = selectedDateInUtcMillis,
+ )
+ )
+ }
+
+ val fullDateDescription = formatWithSkeleton(
+ selectedDateInUtcMillis,
+ DatePickerDefaults.YearMonthWeekdayDaySkeleton,
+ Locale.US
+ )
+
+ rule.onNodeWithText("May 11, 2010")
+ .assertContentDescriptionEquals(
+ expectedHeadlineStringFormat.format(fullDateDescription)
+ )
+ }
+
+ // Returns the given date's day as milliseconds from epoch. The returned value is for the day's
+ // start on midnight.
+ private fun dayInUtcMilliseconds(year: Int, month: Int, dayOfMonth: Int): Long {
+ val firstDayCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
+ firstDayCalendar.clear()
+ firstDayCalendar[Calendar.YEAR] = year
+ firstDayCalendar[Calendar.MONTH] = month - 1
+ firstDayCalendar[Calendar.DAY_OF_MONTH] = dayOfMonth
+ return firstDayCalendar.timeInMillis
+ }
+}
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModel.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModel.android.kt
index a65decb..43ed254 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModel.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModel.android.kt
@@ -18,8 +18,10 @@
import android.os.Build
import android.text.format.DateFormat
-import java.text.SimpleDateFormat
-import java.util.Calendar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.core.os.ConfigurationCompat
import java.util.Locale
/**
@@ -37,6 +39,12 @@
/**
* Formats a UTC timestamp into a string with a given date format skeleton.
*
+ * A skeleton is similar to, and uses the same format characters as described in
+ * [Unicode Technical Standard #35](https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
+ *
+ * One difference is that order is irrelevant. For example, "MMMMd" will return "MMMM d" in the
+ * en_US locale, but "d. MMMM" in the de_CH locale.
+ *
* @param utcTimeMillis a UTC timestamp to format (milliseconds from epoch)
* @param skeleton a date format skeleton
* @param locale the [Locale] to use when formatting the given timestamp
@@ -47,14 +55,21 @@
skeleton: String,
locale: Locale
): String {
+ val pattern = DateFormat.getBestDateTimePattern(locale, skeleton)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- CalendarModelImpl.format(utcTimeMillis, skeleton, locale)
+ CalendarModelImpl.formatWithPattern(utcTimeMillis, pattern, locale)
} else {
- val pattern = DateFormat.getBestDateTimePattern(locale, skeleton)
- val dateFormat = SimpleDateFormat(pattern, locale)
- dateFormat.timeZone = LegacyCalendarModelImpl.utcTimeZone
- val calendar = Calendar.getInstance(LegacyCalendarModelImpl.utcTimeZone)
- calendar.timeInMillis = utcTimeMillis
- dateFormat.format(calendar.timeInMillis)
+ LegacyCalendarModelImpl.formatWithPattern(utcTimeMillis, pattern, locale)
}
}
+
+/**
+ * A composable function that returns the default [Locale]. It will be recomposed when the
+ * `Configuration` gets updated.
+ */
+@Composable
+@ReadOnlyComposable
+@ExperimentalMaterial3Api
+internal actual fun defaultLocale(): Locale {
+ return ConfigurationCompat.getLocales(LocalConfiguration.current).get(0) ?: Locale.getDefault()
+}
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModelImpl.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModelImpl.android.kt
index bb3f95e..f9eed30 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModelImpl.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModelImpl.android.kt
@@ -17,7 +17,6 @@
package androidx.compose.material3
import android.os.Build
-import android.text.format.DateFormat
import androidx.annotation.RequiresApi
import java.time.DayOfWeek
import java.time.Instant
@@ -70,15 +69,16 @@
}
}
- override val dateInputFormat: DateInputFormat
- get() = datePatternAsInputFormat(
+ override fun getDateInputFormat(locale: Locale): DateInputFormat {
+ return datePatternAsInputFormat(
DateTimeFormatterBuilder.getLocalizedDateTimePattern(
/* dateStyle = */ FormatStyle.SHORT,
/* timeStyle = */ null,
- /* chrono = */ Chronology.ofLocale(Locale.getDefault()),
- /* locale = */ Locale.getDefault()
+ /* chrono = */ Chronology.ofLocale(locale),
+ /* locale = */ locale
)
)
+ }
override fun getCanonicalDate(timeInMillis: Long): CalendarDate {
val localDate =
@@ -129,6 +129,9 @@
return getMonth(earlierMonth)
}
+ override fun formatWithPattern(utcTimeMillis: Long, pattern: String, locale: Locale): String =
+ CalendarModelImpl.formatWithPattern(utcTimeMillis, pattern, locale)
+
override fun parse(date: String, pattern: String): CalendarDate? {
// TODO: A DateTimeFormatter can be reused.
val formatter = DateTimeFormatter.ofPattern(pattern)
@@ -149,14 +152,13 @@
companion object {
/**
- * Formats a UTC timestamp into a string with a given date format skeleton.
+ * Formats a UTC timestamp into a string with a given date format pattern.
*
* @param utcTimeMillis a UTC timestamp to format (milliseconds from epoch)
- * @param skeleton a date format skeleton
+ * @param pattern a date format pattern
* @param locale the [Locale] to use when formatting the given timestamp
*/
- fun format(utcTimeMillis: Long, skeleton: String, locale: Locale): String {
- val pattern = DateFormat.getBestDateTimePattern(locale, skeleton)
+ fun formatWithPattern(utcTimeMillis: Long, pattern: String, locale: Locale): String {
val formatter: DateTimeFormatter =
DateTimeFormatter.ofPattern(pattern, locale)
.withDecimalStyle(DecimalStyle.of(locale))
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DatePickerDialog.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DatePickerDialog.android.kt
index cee3ad1..7ad1761 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DatePickerDialog.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DatePickerDialog.android.kt
@@ -43,6 +43,9 @@
* A sample for displaying a [DatePicker] in a dialog:
* @sample androidx.compose.material3.samples.DatePickerDialogSample
*
+ * A sample for displaying a [DateInput] in a dialog:
+ * @sample androidx.compose.material3.samples.DateInputDialogSample
+ *
* @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside
* or pressing the back button. This is not called when the dismiss button is clicked.
* @param confirmButton button which is meant to confirm a proposed action, thus resolving what
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
index 9059aad..44537c3 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
@@ -17,11 +17,15 @@
package androidx.compose.material3
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.R
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
+import androidx.core.os.ConfigurationCompat
+import java.util.Locale
@Composable
+@ReadOnlyComposable
internal actual fun getString(string: Strings): String {
LocalConfiguration.current
val resources = LocalContext.current.resources
@@ -39,32 +43,42 @@
Strings.SnackbarDismiss -> resources.getString(
androidx.compose.material3.R.string.snackbar_dismiss
)
+
Strings.SearchBarSearch -> resources.getString(
androidx.compose.material3.R.string.search_bar_search
)
+
Strings.SuggestionsAvailable ->
resources.getString(androidx.compose.material3.R.string.suggestions_available)
+
Strings.DatePickerTitle -> resources.getString(
androidx.compose.material3.R.string.date_picker_title
)
+
Strings.DatePickerHeadline -> resources.getString(
androidx.compose.material3.R.string.date_picker_headline
)
+
Strings.DatePickerYearPickerPaneTitle -> resources.getString(
androidx.compose.material3.R.string.date_picker_year_picker_pane_title
)
+
Strings.DatePickerSwitchToYearSelection -> resources.getString(
androidx.compose.material3.R.string.date_picker_switch_to_year_selection
)
+
Strings.DatePickerSwitchToDaySelection -> resources.getString(
androidx.compose.material3.R.string.date_picker_switch_to_day_selection
)
+
Strings.DatePickerSwitchToNextMonth -> resources.getString(
androidx.compose.material3.R.string.date_picker_switch_to_next_month
)
+
Strings.DatePickerSwitchToPreviousMonth -> resources.getString(
androidx.compose.material3.R.string.date_picker_switch_to_previous_month
)
+
Strings.DatePickerNavigateToYearDescription -> resources.getString(
androidx.compose.material3.R.string.date_picker_navigate_to_year_description
)
@@ -74,6 +88,41 @@
Strings.DatePickerNoSelectionDescription -> resources.getString(
androidx.compose.material3.R.string.date_picker_no_selection_description
)
+ Strings.DateInputTitle -> resources.getString(
+ androidx.compose.material3.R.string.date_input_title
+ )
+ Strings.DateInputHeadline -> resources.getString(
+ androidx.compose.material3.R.string.date_input_headline
+ )
+ Strings.DateInputLabel -> resources.getString(
+ androidx.compose.material3.R.string.date_input_label
+ )
+ Strings.DateInputHeadlineDescription -> resources.getString(
+ androidx.compose.material3.R.string.date_input_headline_description
+ )
+ Strings.DateInputNoInputHeadlineDescription -> resources.getString(
+ androidx.compose.material3.R.string.date_input_no_input_description
+ )
+ Strings.DateInputInvalidNotAllowed -> resources.getString(
+ androidx.compose.material3.R.string.date_input_invalid_not_allowed
+ )
+ Strings.DateInputInvalidForPattern -> resources.getString(
+ androidx.compose.material3.R.string.date_input_invalid_for_pattern
+ )
+ Strings.DateInputInvalidYearRange -> resources.getString(
+ androidx.compose.material3.R.string.date_input_invalid_year_range
+ )
+ Strings.TooltipLongPressLabel -> resources.getString(
+ androidx.compose.material3.R.string.tooltip_long_press_label
+ )
else -> ""
}
}
+@Composable
+@ReadOnlyComposable
+internal actual fun getString(string: Strings, vararg formatArgs: Any): String {
+ val raw = getString(string)
+ val locale =
+ ConfigurationCompat.getLocales(LocalConfiguration.current).get(0) ?: Locale.getDefault()
+ return String.format(raw, locale, *formatArgs)
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/androidMain/res/values/strings.xml b/compose/material3/material3/src/androidMain/res/values/strings.xml
index 6949cde..dd708e6 100644
--- a/compose/material3/material3/src/androidMain/res/values/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values/strings.xml
@@ -41,4 +41,16 @@
<string name="date_picker_headline_description">Current selection: %1$s</string>
<string name="date_picker_no_selection_description">None</string>
<string name="date_picker_year_picker_pane_title">Year picker visible</string>
+ <string name="date_input_title">Select date</string>
+ <string name="date_input_headline">Entered date</string>
+ <string name="date_input_label">Date</string>
+ <string name="date_input_headline_description">Entered date: %1$s</string>
+ <string name="date_input_no_input_description">None</string>
+ <string name="date_input_invalid_not_allowed">Date not allowed: %1$s</string>
+ <string name="date_input_invalid_for_pattern">Date does not match expected pattern: %1$s</string>
+ <string name="date_input_invalid_year_range">
+ Date out of expected year range %1$s - %2$s
+ </string>
+ <!-- Spoken description of a tooltip -->
+ <string name="tooltip_long_press_label">Show tooltip</string>
</resources>
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt
index 4c06fc7..7bae757 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt
@@ -16,6 +16,8 @@
package androidx.compose.material3
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
import java.util.Locale
/**
@@ -27,6 +29,12 @@
/**
* Formats a UTC timestamp into a string with a given date format skeleton.
*
+ * A skeleton is similar to, and uses the same format characters as described in
+ * [Unicode Technical Standard #35](https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
+ *
+ * One difference is that order is irrelevant. For example, "MMMMd" will return "MMMM d" in the
+ * en_US locale, but "d. MMMM" in the de_CH locale.
+ *
* @param utcTimeMillis a UTC timestamp to format (milliseconds from epoch)
* @param skeleton a date format skeleton
* @param locale the [Locale] to use when formatting the given timestamp
@@ -38,6 +46,16 @@
locale: Locale = Locale.getDefault()
): String
+/**
+ * A composable function that returns the default [Locale].
+ *
+ * When running on an Android platform, it will be recomposed when the `Configuration` gets updated.
+ */
+@Composable
+@ReadOnlyComposable
+@ExperimentalMaterial3Api
+internal expect fun defaultLocale(): Locale
+
@ExperimentalMaterial3Api
internal interface CalendarModel {
@@ -65,7 +83,7 @@
val weekdayNames: List<Pair<String, String>>
/**
- * Holds a [DateInputFormat] for the current [Locale].
+ * Returns a [DateInputFormat] for the given [Locale].
*
* The input format represents the date with two digits for the day and the month, and
* four digits for the year.
@@ -80,7 +98,7 @@
* - dd.MM.yyyy
* - MM/dd/yyyy
*/
- val dateInputFormat: DateInputFormat
+ fun getDateInputFormat(locale: Locale = Locale.getDefault()): DateInputFormat
/**
* Returns a [CalendarDate] from a given _UTC_ time in milliseconds.
@@ -153,7 +171,8 @@
month: CalendarMonth,
skeleton: String,
locale: Locale = Locale.getDefault()
- ): String = formatWithSkeleton(month.startUtcTimeMillis, skeleton, locale)
+ ): String =
+ formatWithSkeleton(month.startUtcTimeMillis, skeleton, locale)
/**
* Formats a [CalendarDate] into a string with a given date format skeleton.
@@ -169,6 +188,15 @@
): String = formatWithSkeleton(date.utcTimeMillis, skeleton, locale)
/**
+ * Formats a UTC timestamp into a string with a given date format pattern.
+ *
+ * @param utcTimeMillis a UTC timestamp to format (milliseconds from epoch)
+ * @param pattern a date format pattern
+ * @param locale the [Locale] to use when formatting the given timestamp
+ */
+ fun formatWithPattern(utcTimeMillis: Long, pattern: String, locale: Locale): String
+
+ /**
* Parses a date string into a [CalendarDate].
*
* @param date a date string
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateInput.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateInput.kt
new file mode 100644
index 0000000..0b0a7e1
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateInput.kt
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.LiveRegionMode
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.error
+import androidx.compose.ui.semantics.liveRegion
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.input.TransformedText
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.unit.dp
+
+// TODO: External preview image.
+// TODO: Introduce a rememberDateInputState once we allow switching between modes.
+/**
+ * <a href="https://m3.material.io/components/date-pickers/overview" class="external" target="_blank">Material Design date input</a>.
+ *
+ * Date pickers let people input a date, and preferably should be embedded into Dialogs.
+ * See [DatePickerDialog].
+ *
+ * A simple DateInput looks like:
+ * @sample androidx.compose.material3.samples.DateInputSample
+ *
+ * @param dateInputState state of the date input. See [rememberDatePickerState].
+ * @param modifier the [Modifier] to be applied to this date input
+ * @param dateFormatter a [DatePickerFormatter] that provides formatting skeletons for dates display
+ * @param dateValidator a lambda that takes a date timestamp and return true if the date is a valid
+ * one for input. Invalid dates will be indicate with an error at the UI.
+ * @param title the title to be displayed in the date input
+ * @param headline the headline to be displayed in the date input
+ * @param colors [DatePickerColors] that will be used to resolve the colors used for this date input
+ * in different states. See [DatePickerDefaults.colors].
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun DateInput(
+ dateInputState: DatePickerState,
+ modifier: Modifier = Modifier,
+ dateFormatter: DatePickerFormatter = remember { DatePickerFormatter() },
+ dateValidator: (Long) -> Boolean = { true },
+ title: (@Composable () -> Unit)? = { DateInputDefaults.DateInputTitle() },
+ headline: @Composable () -> Unit = {
+ DateInputDefaults.DateInputHeadline(
+ dateInputState,
+ dateFormatter
+ )
+ },
+ colors: DatePickerColors = DatePickerDefaults.colors()
+) {
+ Column(modifier = modifier.padding(DatePickerHorizontalPadding)) {
+ // Reusing the same header that is used by the DatePicker.
+ DatePickerHeader(
+ modifier = Modifier,
+ title = title,
+ titleContentColor = colors.titleContentColor,
+ headlineContentColor = colors.headlineContentColor
+ ) {
+ headline()
+ }
+ Divider()
+ DateInputTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(InputTextFieldPadding),
+ dateInputState = dateInputState,
+ dateFormatter = dateFormatter,
+ dateValidator = dateValidator
+ )
+ }
+}
+
+/**
+ * Contains default values used by the date input.
+ */
+@ExperimentalMaterial3Api
+@Stable
+object DateInputDefaults {
+
+ /** A default date input title composable. */
+ @Composable
+ fun DateInputTitle() = Text(getString(string = Strings.DateInputTitle))
+
+ /**
+ * A default date input headline composable lambda that displays a default headline text when
+ * there is no date selection, and an actual date string when there is.
+ *
+ * @param state a [DatePickerState] that will help determine the title's headline
+ * @param dateFormatter a [DatePickerFormatter]
+ */
+ @Composable
+ fun DateInputHeadline(state: DatePickerState, dateFormatter: DatePickerFormatter) {
+ val defaultLocale = defaultLocale()
+ val formattedDate = dateFormatter.formatDate(
+ date = state.selectedDate,
+ calendarModel = state.calendarModel,
+ locale = defaultLocale
+ )
+ val verboseDateDescription = dateFormatter.formatDate(
+ date = state.selectedDate,
+ calendarModel = state.calendarModel,
+ locale = defaultLocale,
+ forContentDescription = true
+ ) ?: getString(Strings.DateInputNoInputHeadlineDescription)
+
+ val headlineText = formattedDate ?: getString(string = Strings.DateInputHeadline)
+ val headlineDescription =
+ getString(Strings.DateInputHeadlineDescription).format(verboseDateDescription)
+
+ Text(
+ text = headlineText,
+ modifier = Modifier.semantics {
+ liveRegion = LiveRegionMode.Polite
+ contentDescription = headlineDescription
+ },
+ maxLines = 1
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun DateInputTextField(
+ modifier: Modifier,
+ dateInputState: DatePickerState,
+ dateFormatter: DatePickerFormatter,
+ dateValidator: (Long) -> Boolean
+) {
+ // Obtain the DateInputFormat for the default Locale.
+ val defaultLocale = defaultLocale()
+ val dateInputFormat = remember(defaultLocale) {
+ dateInputState.calendarModel.getDateInputFormat(defaultLocale)
+ }
+ var errorText by rememberSaveable { mutableStateOf("") }
+ var text by rememberSaveable(stateSaver = TextFieldValue.Saver) {
+ mutableStateOf(
+ TextFieldValue(
+ text = with(dateInputState) {
+ selectedDate?.let {
+ calendarModel.formatWithPattern(
+ it.utcTimeMillis,
+ dateInputFormat.patternWithoutDelimiters,
+ defaultLocale
+ )
+ } ?: ""
+ },
+ TextRange(0, 0)
+ )
+ )
+ }
+
+ // Holds a string for displaying an error message when an input does not match the expected date
+ // pattern. The string expects a date pattern string as an argument to be formatted into it.
+ val errorDatePattern = getString(Strings.DateInputInvalidForPattern)
+ // Holds a string for displaying an error message when an input date exceeds the year-range
+ // defined at the DateInput's state. The string expects a start and end year as arguments to
+ // be formatted into it.
+ val errorDateOutOfYearRange = getString(Strings.DateInputInvalidYearRange)
+ // Holds a string for displaying an error message when an input date does not pass the
+ // DateInput's validator check. The string expects a date argument to be formatted into it.
+ val errorInvalidNotAllowed = getString(Strings.DateInputInvalidNotAllowed)
+
+ // Validates the input. Sets an error message at the errorText, or return a non-null
+ // CalendarDate that represents a validated date.
+ fun validate(input: TextFieldValue): CalendarDate? {
+ val dateInputText = input.text.trim()
+ if (dateInputText.isEmpty() ||
+ dateInputText.length < dateInputFormat.patternWithoutDelimiters.length
+ ) {
+ errorText = ""
+ return null
+ }
+ val parsedDate = dateInputState.calendarModel.parse(
+ dateInputText,
+ dateInputFormat.patternWithoutDelimiters
+ )
+ if (parsedDate == null) {
+ errorText = errorDatePattern.format(dateInputFormat.patternWithDelimiters.uppercase())
+ return null
+ }
+ // Check that the date is within the valid range of years.
+ if (!dateInputState.yearRange.contains(parsedDate.year)) {
+ errorText = errorDateOutOfYearRange.format(
+ dateInputState.yearRange.first,
+ dateInputState.yearRange.last
+ )
+ return null
+ }
+ // Check that the provided date validator allows this date to be selected.
+ if (!dateValidator.invoke(parsedDate.utcTimeMillis)) {
+ errorText = errorInvalidNotAllowed.format(
+ dateFormatter.formatDate(
+ date = parsedDate,
+ calendarModel = dateInputState.calendarModel,
+ locale = defaultLocale
+ )
+ )
+ return null
+ }
+ return parsedDate
+ }
+
+ OutlinedTextField(
+ value = text,
+ onValueChange = { input ->
+ if (input.text.length <= dateInputFormat.patternWithoutDelimiters.length &&
+ input.text.all { it.isDigit() }
+ ) {
+ text = input
+ dateInputState.selectedDate = validate(input)
+ }
+ },
+ modifier = modifier
+ // Add bottom padding when there is no error. Otherwise, remove it as the error text
+ // will take additional height.
+ .padding(
+ bottom = if (errorText.isNotBlank()) {
+ 0.dp
+ } else {
+ InputTextNonErroneousBottomPadding
+ }
+ )
+ .semantics {
+ if (errorText.isNotBlank()) error(errorText)
+ },
+ label = { Text(getString(string = Strings.DateInputLabel)) },
+ placeholder = { Text(dateInputFormat.patternWithDelimiters.uppercase()) },
+ supportingText = { if (errorText.isNotBlank()) Text(errorText) },
+ isError = errorText.isNotBlank(),
+ visualTransformation = DateVisualTransformation(dateInputFormat),
+ keyboardOptions = KeyboardOptions(
+ autoCorrect = false,
+ keyboardType = KeyboardType.Number,
+ imeAction = ImeAction.Done
+ ),
+ singleLine = true
+ )
+}
+
+/**
+ * A [VisualTransformation] for date input. The transformation will automatically display the date
+ * delimiters provided by the [DateInputFormat] as the date is being entered into the text field.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+private class DateVisualTransformation(private val dateInputFormat: DateInputFormat) :
+ VisualTransformation {
+
+ private val firstDelimiterOffset: Int =
+ dateInputFormat.patternWithDelimiters.indexOf(dateInputFormat.delimiter)
+ private val secondDelimiterOffset: Int =
+ dateInputFormat.patternWithDelimiters.lastIndexOf(dateInputFormat.delimiter)
+ private val dateFormatLength: Int = dateInputFormat.patternWithoutDelimiters.length
+
+ private val dateOffsetTranslator = object : OffsetMapping {
+
+ override fun originalToTransformed(offset: Int): Int {
+ return when {
+ offset < firstDelimiterOffset -> offset
+ offset < secondDelimiterOffset -> offset + 1
+ offset <= dateFormatLength -> offset + 2
+ else -> dateFormatLength + 2 // 10
+ }
+ }
+
+ override fun transformedToOriginal(offset: Int): Int {
+ return when {
+ offset <= firstDelimiterOffset - 1 -> offset
+ offset <= secondDelimiterOffset - 1 -> offset - 1
+ offset <= dateFormatLength + 1 -> offset - 2
+ else -> dateFormatLength // 8
+ }
+ }
+ }
+
+ override fun filter(text: AnnotatedString): TransformedText {
+ val trimmedText =
+ if (text.text.length > dateFormatLength) {
+ text.text.substring(0 until dateFormatLength)
+ } else {
+ text.text
+ }
+ var transformedText = ""
+ trimmedText.forEachIndexed { index, char ->
+ transformedText += char
+ if (index + 1 == firstDelimiterOffset || index + 2 == secondDelimiterOffset) {
+ transformedText += dateInputFormat.delimiter
+ }
+ }
+ return TransformedText(AnnotatedString(transformedText), dateOffsetTranslator)
+ }
+}
+
+private val InputTextFieldPadding = PaddingValues(
+ start = 12.dp,
+ end = 12.dp,
+ top = 10.dp
+)
+
+// An optional padding that will only be added to the bottom of the date input text field when it's
+// not showing an error message.
+private val InputTextNonErroneousBottomPadding = 16.dp
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt
index 850dbfc9..fa3d9c1 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt
@@ -302,6 +302,7 @@
* Contains default values used by the date pickers.
*/
@ExperimentalMaterial3Api
+@Stable
object DatePickerDefaults {
/**
@@ -387,16 +388,22 @@
/**
* A default date picker headline composable lambda that displays a default headline text when
* there is no date selection, and an actual date string when there is.
+ *
+ * @param state a [DatePickerState] that will help determine the title's headline
+ * @param dateFormatter a [DatePickerFormatter]
*/
@Composable
fun DatePickerHeadline(state: DatePickerState, dateFormatter: DatePickerFormatter) {
+ val defaultLocale = defaultLocale()
val formattedDate = dateFormatter.formatDate(
date = state.selectedDate,
- calendarModel = state.calendarModel
+ calendarModel = state.calendarModel,
+ locale = defaultLocale
)
val verboseDateDescription = dateFormatter.formatDate(
date = state.selectedDate,
calendarModel = state.calendarModel,
+ locale = defaultLocale,
forContentDescription = true
) ?: getString(Strings.DatePickerNoSelectionDescription)
@@ -658,7 +665,7 @@
internal fun formatMonthYear(
month: CalendarMonth?,
calendarModel: CalendarModel,
- locale: Locale = Locale.getDefault()
+ locale: Locale
): String? {
if (month == null) return null
return calendarModel.formatWithSkeleton(month, yearSelectionSkeleton, locale)
@@ -667,8 +674,8 @@
internal fun formatDate(
date: CalendarDate?,
calendarModel: CalendarModel,
- forContentDescription: Boolean = false,
- locale: Locale = Locale.getDefault()
+ locale: Locale,
+ forContentDescription: Boolean = false
): String? {
if (date == null) return null
return calendarModel.formatWithSkeleton(
@@ -735,13 +742,15 @@
}
var yearPickerVisible by rememberSaveable { mutableStateOf(false) }
+ val defaultLocale = defaultLocale()
MonthsNavigation(
nextAvailable = monthsListState.canScrollForward,
previousAvailable = monthsListState.canScrollBackward,
yearPickerVisible = yearPickerVisible,
yearPickerText = dateFormatter.formatMonthYear(
- datePickerState.displayedMonth,
- datePickerState.calendarModel
+ month = datePickerState.displayedMonth,
+ calendarModel = datePickerState.calendarModel,
+ locale = defaultLocale
) ?: "-",
onNextClicked = {
coroutineScope.launch {
@@ -817,7 +826,7 @@
}
@Composable
-private fun DatePickerHeader(
+internal fun DatePickerHeader(
modifier: Modifier,
title: (@Composable () -> Unit)?,
titleContentColor: Color,
@@ -1037,6 +1046,7 @@
today = dateInMillis == today.utcTimeMillis,
colors = colors
) {
+ val defaultLocale = defaultLocale()
Text(
text = (dayNumber + 1).toLocalString(),
modifier = Modifier.semantics {
@@ -1044,7 +1054,7 @@
formatWithSkeleton(
dateInMillis,
dateFormatter.selectedDateDescriptionSkeleton,
- Locale.getDefault()
+ defaultLocale
)
},
textAlign = TextAlign.Center
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LegacyCalendarModelImpl.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LegacyCalendarModelImpl.kt
index f993d7f..e398668 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LegacyCalendarModelImpl.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LegacyCalendarModelImpl.kt
@@ -61,13 +61,14 @@
add(Pair(weekdays[1], shortWeekdays[1]))
}
- override val dateInputFormat: DateInputFormat
- get() = datePatternAsInputFormat(
+ override fun getDateInputFormat(locale: Locale): DateInputFormat {
+ return datePatternAsInputFormat(
(DateFormat.getDateInstance(
DateFormat.SHORT,
- Locale.getDefault()
+ locale
) as SimpleDateFormat).toPattern()
)
+ }
override fun getCanonicalDate(timeInMillis: Long): CalendarDate {
val calendar = Calendar.getInstance(utcTimeZone)
@@ -124,6 +125,9 @@
return getMonth(earlierMonth)
}
+ override fun formatWithPattern(utcTimeMillis: Long, pattern: String, locale: Locale): String =
+ LegacyCalendarModelImpl.formatWithPattern(utcTimeMillis, pattern, locale)
+
override fun parse(date: String, pattern: String): CalendarDate? {
val dateFormat = SimpleDateFormat(pattern)
dateFormat.timeZone = utcTimeZone
@@ -146,6 +150,21 @@
companion object {
/**
+ * Formats a UTC timestamp into a string with a given date format pattern.
+ *
+ * @param utcTimeMillis a UTC timestamp to format (milliseconds from epoch)
+ * @param pattern a date format pattern
+ * @param locale the [Locale] to use when formatting the given timestamp
+ */
+ fun formatWithPattern(utcTimeMillis: Long, pattern: String, locale: Locale): String {
+ val dateFormat = SimpleDateFormat(pattern, locale)
+ dateFormat.timeZone = utcTimeZone
+ val calendar = Calendar.getInstance(utcTimeZone)
+ calendar.timeInMillis = utcTimeMillis
+ return dateFormat.format(calendar.timeInMillis)
+ }
+
+ /**
* Holds a UTC [TimeZone].
*/
internal val utcTimeZone: TimeZone = TimeZone.getTimeZone("UTC")
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
index f2c6b55..2ed1ace 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
@@ -515,7 +515,7 @@
LinearProgressIndicatorTokens.TrackColor.toColor()
/** Default track color for a circular progress indicator. */
- val circularTrackColor: Color get() = Color.Transparent
+ val circularTrackColor: Color @Composable get() = Color.Transparent
/** Default stroke width for a circular progress indicator. */
val CircularStrokeWidth: Dp = CircularProgressIndicatorTokens.ActiveIndicatorWidth
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
index e9b3e91..7235f1e 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
@@ -143,7 +143,11 @@
* [Interaction]s and customize the appearance / behavior of this slider in different states.
*/
// TODO(b/229979132): Add m.io link
-@OptIn(ExperimentalMaterial3Api::class)
+@Deprecated(
+ message = "Maintained for binary compatibility. " +
+ "Please use the non-experimental API that allows for custom thumb and tracks.",
+ level = DeprecationLevel.HIDDEN
+)
@Composable
fun Slider(
value: Float,
@@ -157,16 +161,17 @@
colors: SliderColors = SliderDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
- Slider(
- value = value,
- onValueChange = onValueChange,
+ require(steps >= 0) { "steps should be >= 0" }
+
+ SliderImpl(
modifier = modifier,
enabled = enabled,
- valueRange = valueRange,
- steps = steps,
- onValueChangeFinished = onValueChangeFinished,
- colors = colors,
interactionSource = interactionSource,
+ onValueChange = onValueChange,
+ onValueChangeFinished = onValueChangeFinished,
+ steps = steps,
+ value = value,
+ valueRange = valueRange,
thumb = {
SliderDefaults.Thumb(
interactionSource = interactionSource,
@@ -224,6 +229,11 @@
* receives a [SliderPositions] which is used to obtain the current active track and the tick positions
* if the slider is discrete.
*/
+@Deprecated(
+ message = "Maintained for binary compatibility. " +
+ "Please use the non-experimental API that allows for custom thumb and tracks.",
+ level = DeprecationLevel.HIDDEN
+)
@Composable
@ExperimentalMaterial3Api
fun Slider(
@@ -239,16 +249,17 @@
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
thumb: @Composable (SliderPositions) -> Unit
) {
- Slider(
- value = value,
- onValueChange = onValueChange,
+ require(steps >= 0) { "steps should be >= 0" }
+
+ SliderImpl(
modifier = modifier,
enabled = enabled,
- valueRange = valueRange,
- steps = steps,
- onValueChangeFinished = onValueChangeFinished,
- colors = colors,
interactionSource = interactionSource,
+ onValueChange = onValueChange,
+ onValueChangeFinished = onValueChangeFinished,
+ steps = steps,
+ value = value,
+ valueRange = valueRange,
thumb = thumb,
track = { sliderPositions ->
SliderDefaults.Track(
@@ -301,6 +312,11 @@
* receives a [SliderPositions] which is used to obtain the current active track and the tick positions
* if the slider is discrete.
*/
+@Deprecated(
+ message = "Maintained for binary compatibility. " +
+ "Please use the non-experimental API that allows for custom thumb and tracks.",
+ level = DeprecationLevel.HIDDEN
+)
@Composable
@ExperimentalMaterial3Api
fun Slider(
@@ -340,7 +356,195 @@
}
/**
- * Material Design Range slider
+ * <a href="https://m3.material.io/components/sliders/overview" class="external" target="_blank">Material Design slider</a>.
+ *
+ * Sliders allow users to make selections from a range of values.
+ *
+ * Sliders reflect a range of values along a bar, from which users may select a single value.
+ * They are ideal for adjusting settings such as volume, brightness, or applying image filters.
+ *
+ * 
+ *
+ * Use continuous sliders to allow users to make meaningful selections that don’t
+ * require a specific value:
+ *
+ * @sample androidx.compose.material3.samples.SliderSample
+ *
+ * You can allow the user to choose only between predefined set of values by specifying the amount
+ * of steps between min and max values:
+ *
+ * @sample androidx.compose.material3.samples.StepsSliderSample
+ *
+ * Slider using a custom thumb:
+ *
+ * @sample androidx.compose.material3.samples.SliderWithCustomThumbSample
+ *
+ * Slider using custom track and thumb:
+ *
+ * @sample androidx.compose.material3.samples.SliderWithCustomTrackAndThumb
+ *
+ * @param value current value of the slider. If outside of [valueRange] provided, value will be
+ * coerced to this range.
+ * @param onValueChange callback in which value should be updated
+ * @param modifier the [Modifier] to be applied to this slider
+ * @param enabled controls the enabled state of this slider. When `false`, this component will not
+ * respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services.
+ * @param valueRange range of values that this slider can take. The passed [value] will be coerced
+ * to this range.
+ * @param onValueChangeFinished called when value change has ended. This should not be used to
+ * update the slider value (use [onValueChange] instead), but rather to know when the user has
+ * completed selecting a new value by ending a drag or a click.
+ * @param colors [SliderColors] that will be used to resolve the colors used for this slider in
+ * different states. See [SliderDefaults.colors].
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this slider. You can create and pass in your own `remember`ed instance to observe
+ * [Interaction]s and customize the appearance / behavior of this slider in different states.
+ * @param thumb the thumb to be displayed on the slider, it is placed on top of the track. The lambda
+ * receives a [SliderPositions] which is used to obtain the current active track and the tick positions
+ * if the slider is discrete.
+ * @param track the track to be displayed on the slider, it is placed underneath the thumb. The lambda
+ * receives a [SliderPositions] which is used to obtain the current active track and the tick positions
+ * if the slider is discrete.
+ * @param steps if greater than 0, specifies the amount of discrete allowable values, evenly
+ * distributed across the whole value range. If 0, the slider will behave continuously and allow any
+ * value from the range specified. Must not be negative.
+ */
+@Composable
+fun Slider(
+ value: Float,
+ onValueChange: (Float) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
+ onValueChangeFinished: (() -> Unit)? = null,
+ colors: SliderColors = SliderDefaults.colors(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ thumb: @Composable (SliderPositions) -> Unit = {
+ SliderDefaults.Thumb(
+ interactionSource = interactionSource,
+ colors = colors,
+ enabled = enabled
+ )
+ },
+ track: @Composable (SliderPositions) -> Unit = { sliderPositions ->
+ SliderDefaults.Track(
+ colors = colors,
+ enabled = enabled,
+ sliderPositions = sliderPositions
+ )
+ },
+ /*@IntRange(from = 0)*/
+ steps: Int = 0,
+) {
+ require(steps >= 0) { "steps should be >= 0" }
+
+ SliderImpl(
+ value = value,
+ onValueChange = onValueChange,
+ modifier = modifier,
+ enabled = enabled,
+ valueRange = valueRange,
+ steps = steps,
+ onValueChangeFinished = onValueChangeFinished,
+ interactionSource = interactionSource,
+ thumb = thumb,
+ track = track
+ )
+}
+
+/**
+ * <a href="https://m3.material.io/components/sliders/overview" class="external" target="_blank">Material Design Range slider</a>.
+ *
+ * Range Sliders expand upon [Slider] using the same concepts but allow the user to select 2 values.
+ *
+ * The two values are still bounded by the value range but they also cannot cross each other.
+ *
+ * Use continuous Range Sliders to allow users to make meaningful selections that don’t
+ * require a specific values:
+ *
+ * @sample androidx.compose.material3.samples.RangeSliderSample
+ *
+ * You can allow the user to choose only between predefined set of values by specifying the amount
+ * of steps between min and max values:
+ *
+ * @sample androidx.compose.material3.samples.StepRangeSliderSample
+ *
+ * @param value current values of the RangeSlider. If either value is outside of [valueRange]
+ * provided, it will be coerced to this range.
+ * @param onValueChange lambda in which values should be updated
+ * @param modifier modifiers for the Range Slider layout
+ * @param enabled whether or not component is enabled and can we interacted with or not
+ * @param valueRange range of values that Range Slider values can take. Passed [value] will be
+ * coerced to this range
+ * @param steps if greater than 0, specifies the amounts of discrete values, evenly distributed
+ * between across the whole value range. If 0, range slider will behave as a continuous slider and
+ * allow to choose any value from the range specified. Must not be negative.
+ * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
+ * shouldn't be used to update the range slider values (use [onValueChange] for that), but rather to
+ * know when the user has completed selecting a new value by ending a drag or a click.
+ * @param colors [SliderColors] that will be used to determine the color of the Range Slider
+ * parts in different state. See [SliderDefaults.colors] to customize.
+ */
+@Deprecated(
+ message = "Maintained for binary compatibility. " +
+ "Please use the non-experimental API that allows for custom thumbs and tracks.",
+ level = DeprecationLevel.HIDDEN
+)
+@Composable
+@ExperimentalMaterial3Api
+fun RangeSlider(
+ value: ClosedFloatingPointRange<Float>,
+ onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
+ /*@IntRange(from = 0)*/
+ steps: Int = 0,
+ onValueChangeFinished: (() -> Unit)? = null,
+ colors: SliderColors = SliderDefaults.colors()
+) {
+ val startInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+ val endInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+
+ require(steps >= 0) { "steps should be >= 0" }
+
+ RangeSliderImpl(
+ modifier = modifier,
+ value = value,
+ onValueChange = onValueChange,
+ enabled = enabled,
+ valueRange = valueRange,
+ steps = steps,
+ onValueChangeFinished = onValueChangeFinished,
+ startInteractionSource = startInteractionSource,
+ endInteractionSource = endInteractionSource,
+ startThumb = {
+ SliderDefaults.Thumb(
+ interactionSource = startInteractionSource,
+ colors = colors,
+ enabled = enabled
+ )
+ },
+ endThumb = {
+ SliderDefaults.Thumb(
+ interactionSource = endInteractionSource,
+ colors = colors,
+ enabled = enabled
+ )
+ },
+ track = { sliderPositions ->
+ SliderDefaults.Track(
+ colors = colors,
+ enabled = enabled,
+ sliderPositions = sliderPositions
+ )
+ }
+ )
+}
+
+/**
+ * <a href="https://m3.material.io/components/sliders/overview" class="external" target="_blank">Material Design Range slider</a>.
*
* Range Sliders expand upon [Slider] using the same concepts but allow the user to select 2 values.
*
@@ -397,7 +601,6 @@
* tick positions if the range slider is discrete.
*/
@Composable
-@ExperimentalMaterial3Api
fun RangeSlider(
value: ClosedFloatingPointRange<Float>,
onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
@@ -450,7 +653,6 @@
)
}
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SliderImpl(
modifier: Modifier,
@@ -600,7 +802,6 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RangeSliderImpl(
modifier: Modifier,
@@ -929,7 +1130,6 @@
* accessibility services.
*/
@Composable
- @ExperimentalMaterial3Api
fun Thumb(
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
@@ -987,7 +1187,6 @@
* accessibility services.
*/
@Composable
- @ExperimentalMaterial3Api
fun Track(
sliderPositions: SliderPositions,
modifier: Modifier = Modifier,
@@ -1454,7 +1653,6 @@
* and fractional positions where the discrete ticks should be drawn on the track.
*/
@Stable
-@ExperimentalMaterial3Api
class SliderPositions(
initialActiveRange: ClosedFloatingPointRange<Float> = 0f..1f,
initialTickFractions: FloatArray = floatArrayOf()
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
index 0d58497..477824a 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
@@ -18,6 +18,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.ReadOnlyComposable
@Immutable
@kotlin.jvm.JvmInline
@@ -46,8 +47,22 @@
val DatePickerNavigateToYearDescription = Strings(20)
val DatePickerHeadlineDescription = Strings(21)
val DatePickerNoSelectionDescription = Strings(22)
+ val DateInputTitle = Strings(23)
+ val DateInputHeadline = Strings(24)
+ val DateInputLabel = Strings(25)
+ val DateInputHeadlineDescription = Strings(26)
+ val DateInputNoInputHeadlineDescription = Strings(27)
+ val DateInputInvalidNotAllowed = Strings(28)
+ val DateInputInvalidForPattern = Strings(29)
+ val DateInputInvalidYearRange = Strings(30)
+ val TooltipLongPressLabel = Strings(31)
}
}
@Composable
+@ReadOnlyComposable
internal expect fun getString(string: Strings): String
+
+@Composable
+@ReadOnlyComposable
+internal expect fun getString(string: Strings, vararg formatArgs: Any): String
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
index d6f7948..43f3a6f 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
@@ -28,7 +28,6 @@
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
@@ -53,6 +52,7 @@
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
+import androidx.compose.ui.semantics.onLongClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
@@ -62,7 +62,6 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -80,10 +79,7 @@
*
* @param tooltip the composable that will be used to populate the tooltip's content.
* @param modifier the [Modifier] to be applied to the tooltip.
- * @param tooltipState handles the state of the tooltip's visibility. If nothing is passed to
- * tooltipState the tooltip will trigger on long press of the anchor content. If control
- * of when the tooltip is triggered is desired pass in a [TooltipState], please note that
- * this will deactivate the default behavior of triggering on long press of the anchor content.
+ * @param tooltipState handles the state of the tooltip's visibility.
* @param shape the [Shape] that should be applied to the tooltip container.
* @param containerColor [Color] that will be applied to the tooltip's container.
* @param contentColor [Color] that will be applied to the tooltip's content.
@@ -94,17 +90,14 @@
fun PlainTooltipBox(
tooltip: @Composable () -> Unit,
modifier: Modifier = Modifier,
- tooltipState: TooltipState? = null,
+ tooltipState: TooltipState = remember { TooltipState() },
shape: Shape = TooltipDefaults.plainTooltipContainerShape,
containerColor: Color = TooltipDefaults.plainTooltipContainerColor,
contentColor: Color = TooltipDefaults.plainTooltipContentColor,
- content: @Composable BoxScope.() -> Unit
+ content: @Composable TooltipBoxScope.() -> Unit
) {
val tooltipAnchorPadding = with(LocalDensity.current) { TooltipAnchorPadding.roundToPx() }
val positionProvider = remember { PlainTooltipPositionProvider(tooltipAnchorPadding) }
- val scope = rememberCoroutineScope()
- val showOnLongPress = tooltipState == null
- val state = tooltipState ?: remember { TooltipState() }
TooltipBox(
tooltipContent = {
@@ -114,21 +107,13 @@
)
},
modifier = modifier,
- tooltipState = state,
- scope = scope,
+ tooltipState = tooltipState,
shape = shape,
containerColor = containerColor,
tooltipPositionProvider = positionProvider,
elevation = 0.dp,
maxWidth = PlainTooltipMaxWidth,
- content = if (showOnLongPress) { {
- Box(
- modifier = Modifier.appendLongClick(
- onLongClick = { scope.launch { state.show() } }
- ),
- content = content
- )
- } } else content
+ content = content
)
}
@@ -140,18 +125,54 @@
modifier: Modifier,
shape: Shape,
tooltipState: TooltipState,
- scope: CoroutineScope,
containerColor: Color,
elevation: Dp,
maxWidth: Dp,
- content: @Composable BoxScope.() -> Unit,
+ content: @Composable TooltipBoxScope.() -> Unit,
) {
+ val coroutineScope = rememberCoroutineScope()
+ val longPressLabel = getString(string = Strings.TooltipLongPressLabel)
+
+ val scope = remember {
+ object : TooltipBoxScope {
+ override fun Modifier.tooltipAnchor(): Modifier {
+ return pointerInput(tooltipState) {
+ awaitEachGesture {
+ val longPressTimeout = viewConfiguration.longPressTimeoutMillis
+ val pass = PointerEventPass.Initial
+
+ // wait for the first down press
+ awaitFirstDown(pass = pass)
+
+ try {
+ // listen to if there is up gesture within the longPressTimeout limit
+ withTimeout(longPressTimeout) {
+ waitForUpOrCancellation(pass = pass)
+ }
+ } catch (_: PointerEventTimeoutCancellationException) {
+ // handle long press - Show the tooltip
+ coroutineScope.launch {
+ tooltipState.show()
+ }
+
+ // consume the children's click handling
+ val event = awaitPointerEvent(pass = pass)
+ event.changes.forEach { it.consume() }
+ }
+ }
+ }.semantics(mergeDescendants = true) {
+ onLongClick(label = longPressLabel, action = null)
+ }
+ }
+ }
+ }
+
Box {
Popup(
popupPositionProvider = tooltipPositionProvider,
onDismissRequest = {
if (tooltipState.isVisible) {
- scope.launch { tooltipState.dismiss() }
+ coroutineScope.launch { tooltipState.dismiss() }
}
}
) {
@@ -172,7 +193,7 @@
)
}
- content()
+ scope.content()
}
}
@@ -291,31 +312,18 @@
)
}
-private fun Modifier.appendLongClick(
- onLongClick: () -> Unit
-) = this.pointerInput(Unit) {
- awaitEachGesture {
- val longPressTimeout = viewConfiguration.longPressTimeoutMillis
- val pass = PointerEventPass.Initial
-
- // wait for the first down press
- awaitFirstDown(pass = pass)
-
- try {
- // listen to if there is up gesture within the longPressTimeout limit
- withTimeout(longPressTimeout) {
- waitForUpOrCancellation(pass = pass)
- }
- } catch (_: PointerEventTimeoutCancellationException) {
- // handle long press
- onLongClick()
-
- // consume the children's click handling
- val event = awaitPointerEvent(pass = pass)
- event.changes.forEach { it.consume() }
- }
- }
- }
+/**
+ * Scope of [PlainTooltipBox] and RichTooltipBox
+ */
+@ExperimentalMaterial3Api
+interface TooltipBoxScope {
+ /**
+ * [Modifier] that should be applied to the anchor composable when showing the tooltip
+ * after long pressing the anchor composable is desired. It appends a long click to
+ * the composable that this modifier is chained with.
+ */
+ fun Modifier.tooltipAnchor(): Modifier
+}
/**
* The state that is associated with an instance of a tooltip.
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/CalendarModel.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/CalendarModel.desktop.kt
index a82844f..7d35284 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/CalendarModel.desktop.kt
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/CalendarModel.desktop.kt
@@ -16,8 +16,8 @@
package androidx.compose.material3
-import java.text.SimpleDateFormat
-import java.util.Calendar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
import java.util.Locale
/**
@@ -42,9 +42,17 @@
// Note: there is no equivalent in Java for Android's DateFormat.getBestDateTimePattern.
// The JDK SimpleDateFormat expects a pattern, so the results will be "2023Jan7",
// "2023January", etc. in case a skeleton holds an actual ICU skeleton and not a pattern.
- val dateFormat = SimpleDateFormat(skeleton, locale)
- dateFormat.timeZone = LegacyCalendarModelImpl.utcTimeZone
- val calendar = Calendar.getInstance(LegacyCalendarModelImpl.utcTimeZone)
- calendar.timeInMillis = utcTimeMillis
- return dateFormat.format(calendar.timeInMillis)
+ return LegacyCalendarModelImpl.formatWithPattern(
+ utcTimeMillis = utcTimeMillis,
+ pattern = skeleton,
+ locale = locale
+ )
}
+
+/**
+ * A composable function that returns the default [Locale].
+ */
+@Composable
+@ReadOnlyComposable
+@ExperimentalMaterial3Api
+internal actual fun defaultLocale(): Locale = Locale.getDefault()
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
index 398da25..132d0b1 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
@@ -17,8 +17,12 @@
package androidx.compose.material3
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+
+import java.util.Locale
@Composable
+@ReadOnlyComposable
internal actual fun getString(string: Strings): String {
return when (string) {
Strings.NavigationMenu -> "Navigation menu"
@@ -44,6 +48,19 @@
Strings.DatePickerNavigateToYearDescription -> "Navigate to year %1$"
Strings.DatePickerHeadlineDescription -> "Current selection: %1$"
Strings.DatePickerNoSelectionDescription -> "None"
+ Strings.DateInputTitle -> "Select date"
+ Strings.DateInputHeadline -> "Entered date"
+ Strings.DateInputLabel -> "Date"
+ Strings.DateInputHeadlineDescription -> "Entered date: %1$"
+ Strings.DateInputNoInputHeadlineDescription -> "None"
+ Strings.DateInputInvalidNotAllowed -> "Date not allowed: %1$"
+ Strings.DateInputInvalidForPattern -> "Date does not match expected pattern: %1$"
+ Strings.DateInputInvalidYearRange -> "Date out of expected year range %1$ - %2$"
+ Strings.TooltipLongPressLabel -> "Show tooltip"
else -> ""
}
}
+@Composable
+@ReadOnlyComposable
+internal actual fun getString(string: Strings, vararg formatArgs: Any): String =
+ String.format(getString(string), Locale.getDefault(), *formatArgs)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index 25e87d6..94957ba 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -743,13 +743,13 @@
applied = true
// Notify any apply observers that changes applied were seen
- if (globalModified != null && globalModified.isNotEmpty()) {
+ if (!globalModified.isNullOrEmpty()) {
observers.fastForEach {
it(globalModified, this)
}
}
- if (modified != null && modified.isNotEmpty()) {
+ if (!modified.isNullOrEmpty()) {
observers.fastForEach {
it(modified, this)
}
@@ -760,6 +760,9 @@
// before unpinning records that need to be retained in this case.
sync {
releasePinnedSnapshotsForCloseLocked()
+
+ globalModified?.forEach(::overwriteUnusedRecordsLocked)
+ modified?.forEach(::overwriteUnusedRecordsLocked)
}
return SnapshotApplyResult.Success
@@ -1117,9 +1120,9 @@
* records that are already in the list cannot be moved in the list as this the change must
* be atomic to all threads that cannot happen without a lock which this list cannot afford.
*
- * It is unsafe to remove a record as it might be in the process of being reused (see [used]).
+ * It is unsafe to remove a record as it might be in the process of being reused (see [usedLocked]).
* If a record is removed care must be taken to ensure that it is not being claimed by some
- * other thread. This would require changes to [used].
+ * other thread. This would require changes to [usedLocked].
*/
internal var next: StateRecord? = null
@@ -1766,6 +1769,10 @@
}
}
+ sync {
+ modified?.forEach(::overwriteUnusedRecordsLocked)
+ }
+
return result
}
@@ -1838,8 +1845,9 @@
// or will find a valid record. Being in a sync block prevents other threads from writing
// to this state object until the read completes.
val syncSnapshot = Snapshot.current
- readable(this, syncSnapshot.id, syncSnapshot.invalid)
- } ?: readError()
+ @Suppress("UNCHECKED_CAST")
+ readable(state.firstStateRecord as T, syncSnapshot.id, syncSnapshot.invalid) ?: readError()
+ }
}
/**
@@ -1864,7 +1872,7 @@
* record created in an abandoned snapshot. It is also true if the record is valid in the
* previous snapshot and is obscured by another record also valid in the previous state record.
*/
-private fun used(state: StateObject): StateRecord? {
+private fun usedLocked(state: StateObject): StateRecord? {
var current: StateRecord? = state.firstStateRecord
var validRecord: StateRecord? = null
val reuseLimit = pinningTable.lowestOrDefault(nextSnapshotId) - 1
@@ -1872,8 +1880,8 @@
while (current != null) {
val currentId = current.snapshotId
if (currentId == INVALID_SNAPSHOT) {
- // Any records that were marked invalid by an abandoned snapshot can be used
- // immediately.
+ // Any records that were marked invalid by an abandoned snapshot or is marked reachable
+ // can be used immediately.
return current
}
if (valid(current, reuseLimit, invalid)) {
@@ -1890,6 +1898,56 @@
return null
}
+/**
+ * Clear records that cannot be selected in any currently open snapshot.
+ *
+ * This method uses the same technique as [usedLocked] which uses the [pinningTable] to
+ * determine lowest id in the invalid set for all snapshots. Only the record with the greatest
+ * id of all records less or equal to this lowest id can possibly be selected in any snapshot
+ * and all other records below that number can be overwritten.
+ *
+ * However, this technique doesn't find all records that will not be selected by any open snapshot
+ * as a record that has an id above that number could be reusable but will not be found.
+ *
+ * For example if snapshot 1 is open and 2 is created and modifies [state] then is applied, 3 is
+ * open and then 4 is open, and then 1 is applied. When 3 modifies [state] and then applies, as 1 is
+ * pinned by 4, it is uncertain whether the record for 2 is needed by 4 so it must be kept even if 4
+ * also modified [state] and would not select 2. Accurately determine if a record is selectable
+ * would require keeping a list of all open [Snapshot] instances which currently is not kept and
+ * would require keeping a list of all open [Snapshot] instances which currently is not kept and
+ * traversing that list for each record.
+ *
+ * If any such records are possible this method returns true. In other words, this method returns
+ * true if any records might be reusable but this function could not prove there were or not.
+ */
+private fun overwriteUnusedRecordsLocked(state: StateObject): Boolean {
+ var current: StateRecord? = state.firstStateRecord
+ var validRecord: StateRecord? = null
+ val reuseLimit = pinningTable.lowestOrDefault(nextSnapshotId) - 1
+ var uncertainRecords = 0
+ while (current != null) {
+ val currentId = current.snapshotId
+ if (currentId != INVALID_SNAPSHOT) {
+ if (currentId <= reuseLimit) {
+ if (validRecord == null) {
+ validRecord = current
+ } else {
+ val recordToOverwrite = if (current.snapshotId < validRecord.snapshotId) {
+ current
+ } else {
+ validRecord.also { validRecord = current }
+ }
+ recordToOverwrite.snapshotId = INVALID_SNAPSHOT
+ validRecord?.let { recordToOverwrite.assign(it) }
+ }
+ } else uncertainRecords++
+ }
+ current = current.next
+ }
+
+ return uncertainRecords < 1
+}
+
@PublishedApi
internal fun <T : StateRecord> T.writableRecord(state: StateObject, snapshot: Snapshot): T {
if (snapshot.readOnly) {
@@ -1924,7 +1982,7 @@
if (candidate.snapshotId == id) return candidate
- val newData = newOverwritableRecord(state)
+ val newData = sync { newOverwritableRecordLocked(state) }
newData.snapshotId = id
snapshot.recordModified(state)
@@ -1932,7 +1990,10 @@
return newData
}
-internal fun <T : StateRecord> T.newWritableRecord(state: StateObject, snapshot: Snapshot): T {
+internal fun <T : StateRecord> T.newWritableRecord(state: StateObject, snapshot: Snapshot) =
+ sync { newWritableRecordLocked(state, snapshot) }
+
+private fun <T : StateRecord> T.newWritableRecordLocked(state: StateObject, snapshot: Snapshot): T {
// Calling used() on a state object might return the same record for each thread calling
// used() therefore selecting the record to reuse should be guarded.
@@ -1945,13 +2006,13 @@
// cache the result of readable() as the mutating thread calls to writable() can change the
// result of readable().
@Suppress("UNCHECKED_CAST")
- val newData = newOverwritableRecord(state)
+ val newData = newOverwritableRecordLocked(state)
newData.assign(this)
newData.snapshotId = snapshot.id
return newData
}
-internal fun <T : StateRecord> T.newOverwritableRecord(state: StateObject): T {
+internal fun <T : StateRecord> T.newOverwritableRecordLocked(state: StateObject): T {
// Calling used() on a state object might return the same record for each thread calling
// used() therefore selecting the record to reuse should be guarded.
@@ -1964,7 +2025,7 @@
// cache the result of readable() as the mutating thread calls to writable() can change the
// result of readable().
@Suppress("UNCHECKED_CAST")
- return (used(state) as T?)?.apply {
+ return (usedLocked(state) as T?)?.apply {
snapshotId = Int.MAX_VALUE
} ?: create().apply {
snapshotId = Int.MAX_VALUE
diff --git a/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTestsJvm.kt b/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTestsJvm.kt
index 307a8eb..da4987d 100644
--- a/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTestsJvm.kt
+++ b/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTestsJvm.kt
@@ -72,6 +72,7 @@
running.set(false)
}
+ exception.get()?.let { throw it }
assertNull(exception.get())
}
}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index fd2b2c0..358b6e6 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -734,7 +734,7 @@
}
public final class RectHelper_androidKt {
- method public static android.graphics.Rect toAndroidRect(androidx.compose.ui.geometry.Rect);
+ method @Deprecated public static android.graphics.Rect toAndroidRect(androidx.compose.ui.geometry.Rect);
method public static android.graphics.Rect toAndroidRect(androidx.compose.ui.unit.IntRect);
method public static android.graphics.RectF toAndroidRectF(androidx.compose.ui.geometry.Rect);
method public static androidx.compose.ui.unit.IntRect toComposeIntRect(android.graphics.Rect);
diff --git a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
index b9e2ae2..39ec0533 100644
--- a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
@@ -737,7 +737,7 @@
}
public final class RectHelper_androidKt {
- method public static android.graphics.Rect toAndroidRect(androidx.compose.ui.geometry.Rect);
+ method @Deprecated public static android.graphics.Rect toAndroidRect(androidx.compose.ui.geometry.Rect);
method public static android.graphics.Rect toAndroidRect(androidx.compose.ui.unit.IntRect);
method public static android.graphics.RectF toAndroidRectF(androidx.compose.ui.geometry.Rect);
method public static androidx.compose.ui.unit.IntRect toComposeIntRect(android.graphics.Rect);
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index c59dbea..5dbda20 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -766,7 +766,7 @@
}
public final class RectHelper_androidKt {
- method public static android.graphics.Rect toAndroidRect(androidx.compose.ui.geometry.Rect);
+ method @Deprecated public static android.graphics.Rect toAndroidRect(androidx.compose.ui.geometry.Rect);
method public static android.graphics.Rect toAndroidRect(androidx.compose.ui.unit.IntRect);
method public static android.graphics.RectF toAndroidRectF(androidx.compose.ui.geometry.Rect);
method public static androidx.compose.ui.unit.IntRect toComposeIntRect(android.graphics.Rect);
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/RectHelperTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/RectHelperTest.kt
index 248e171..8c20469 100644
--- a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/RectHelperTest.kt
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/RectHelperTest.kt
@@ -28,6 +28,7 @@
@RunWith(AndroidJUnit4::class)
class RectHelperTest {
+ @Suppress("DEPRECATION")
@Test
fun rectToAndroidRectTruncates() {
assertEquals(
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
index 5e79774..0907e2c 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
@@ -133,12 +133,12 @@
override fun addRect(rect: Rect) {
check(_rectIsValid(rect))
- rectF.set(rect.toAndroidRectF())
+ rectF.set(rect.left, rect.top, rect.right, rect.bottom)
internalPath.addRect(rectF, android.graphics.Path.Direction.CCW)
}
override fun addOval(oval: Rect) {
- rectF.set(oval.toAndroidRect())
+ rectF.set(oval.left, oval.top, oval.right, oval.bottom)
internalPath.addOval(rectF, android.graphics.Path.Direction.CCW)
}
@@ -148,7 +148,7 @@
override fun addArc(oval: Rect, startAngleDegrees: Float, sweepAngleDegrees: Float) {
check(_rectIsValid(oval))
- rectF.set(oval.toAndroidRect())
+ rectF.set(oval.left, oval.top, oval.right, oval.bottom)
internalPath.addArc(rectF, startAngleDegrees, sweepAngleDegrees)
}
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/RectHelper.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/RectHelper.android.kt
index c926c3c..9537090 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/RectHelper.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/RectHelper.android.kt
@@ -22,6 +22,14 @@
* Creates a new instance of [android.graphics.Rect] with the same bounds
* specified in the given [Rect]
*/
+@Deprecated(
+ "Converting Rect to android.graphics.Rect is lossy, and requires rounding. The " +
+ "behavior of toAndroidRect() truncates to an integral Rect, but you should choose the " +
+ "method of rounding most suitable for your use case.",
+ replaceWith = ReplaceWith(
+ "android.graphics.Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())"
+ )
+)
fun Rect.toAndroidRect(): android.graphics.Rect {
return android.graphics.Rect(
left.toInt(),
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
index b3cd8cd..00de43db 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
@@ -246,8 +246,6 @@
* because we reached `maxLines` lines of text or because the `maxLines` was
* null, `ellipsis` was not null, and one of the lines exceeded the width
* constraint.
- *
- * See the discussion of the `maxLines` and `ellipsis` arguments at [ParagraphStyle].
*/
val didExceedMaxLines: Boolean
@@ -779,7 +777,7 @@
private fun requireLineIndexInRange(lineIndex: Int) {
require(lineIndex in 0 until lineCount) {
- "lineIndex($lineIndex) is out of bounds [0, $lineIndex)"
+ "lineIndex($lineIndex) is out of bounds [0, $lineCount)"
}
}
}
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt
index 62ec3a6..541262b 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt
@@ -646,9 +646,9 @@
}
private val FakeViewModelStoreOwner = object : ViewModelStoreOwner {
- private val viewModelStore = ViewModelStore()
+ private val vmStore = ViewModelStore()
- override fun getViewModelStore() = viewModelStore
+ override val viewModelStore = vmStore
}
private val FakeOnBackPressedDispatcherOwner = object : OnBackPressedDispatcherOwner {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index 9af6a59..1e844e4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -86,8 +86,8 @@
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.ImageBitmap
-import androidx.compose.ui.graphics.toAndroidRect
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.toComposeRect
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.AndroidComposeView
import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat
@@ -1150,7 +1150,7 @@
textFieldNode.positionInWindow
)
val expectedTopLeftInScreenCoords = androidComposeView.localToScreen(
- expectedRectInLocalCoords.toAndroidRect().topLeftToOffset()
+ expectedRectInLocalCoords.topLeft
)
assertEquals(expectedTopLeftInScreenCoords.x, rectF.left)
assertEquals(expectedTopLeftInScreenCoords.y, rectF.top)
@@ -2932,7 +2932,7 @@
accessibilityNodeInfo.getBoundsInScreen(rect)
val resultWidth = rect.right - rect.left
val resultHeight = rect.bottom - rect.top
- val resultInLocalCoords = androidComposeView.screenToLocal(rect.topLeftToOffset())
+ val resultInLocalCoords = androidComposeView.screenToLocal(rect.toComposeRect().topLeft)
assertEquals(size, resultWidth)
assertEquals(size, resultHeight)
@@ -3466,5 +3466,3 @@
)
}
}
-
-private fun Rect.topLeftToOffset() = Offset(this.left.toFloat(), this.top.toFloat())
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt
index a1b44c7..436c8be 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt
@@ -27,7 +27,6 @@
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.graphics.toAndroidRect
import androidx.compose.ui.util.fastMap
/**
@@ -46,13 +45,20 @@
init { view.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_YES }
override fun requestAutofillForNode(autofillNode: AutofillNode) {
+ val boundingBox = autofillNode.boundingBox
+ ?: error("requestAutofill called before onChildPositioned()")
+
// TODO(b/138731416): Find out what happens when notifyViewEntered() is called multiple times
// before calling notifyViewExited().
autofillManager.notifyViewEntered(
view,
autofillNode.id,
- autofillNode.boundingBox?.toAndroidRect()
- ?: error("requestAutofill called before onChildPositioned()")
+ android.graphics.Rect(
+ boundingBox.left.toInt(),
+ boundingBox.top.toInt(),
+ boundingBox.right.toInt(),
+ boundingBox.bottom.toInt()
+ )
)
}
@@ -89,7 +95,8 @@
autofillNode.autofillTypes.fastMap { it.androidType }.toTypedArray()
)
- if (autofillNode.boundingBox == null) {
+ val boundingBox = autofillNode.boundingBox
+ if (boundingBox == null) {
// Do we need an exception here? warning? silently ignore? If the boundingbox is
// null, the autofill overlay will not be shown.
Log.w(
@@ -97,9 +104,14 @@
"""Bounding box not set.
Did you call perform autofillTree before the component was positioned? """
)
- }
- autofillNode.boundingBox?.toAndroidRect()?.run {
- AutofillApi23Helper.setDimens(child, left, top, 0, 0, width(), height())
+ } else {
+ val left = boundingBox.left.toInt()
+ val top = boundingBox.top.toInt()
+ val right = boundingBox.right.toInt()
+ val bottom = boundingBox.bottom.toInt()
+ val width = right - left
+ val height = bottom - top
+ AutofillApi23Helper.setDimens(child, left, top, 0, 0, width, height)
}
}
index++
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index f2876ab..64e9bbe 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -49,7 +49,6 @@
import androidx.compose.ui.fastJoinToString
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.graphics.toAndroidRect
import androidx.compose.ui.graphics.toComposeRect
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.boundsInWindow
@@ -2797,7 +2796,18 @@
if (!root.layoutNode.isPlaced || !root.layoutNode.isAttached) {
return nodes
}
- val unaccountedSpace = Region().also { it.set(root.boundsInRoot.toAndroidRect()) }
+ val unaccountedSpace = Region().also {
+ it.set(
+ root.boundsInRoot.run {
+ android.graphics.Rect(
+ left.toInt(),
+ top.toInt(),
+ right.toInt(),
+ bottom.toInt()
+ )
+ }
+ )
+ }
fun findAllSemanticNodesRecursive(currentNode: SemanticsNode) {
val notAttachedOrPlaced =
@@ -2807,7 +2817,12 @@
) {
return
}
- val boundsInRoot = currentNode.touchBoundsInRoot.toAndroidRect()
+ val boundsInRoot = android.graphics.Rect(
+ currentNode.touchBoundsInRoot.left.toInt(),
+ currentNode.touchBoundsInRoot.top.toInt(),
+ currentNode.touchBoundsInRoot.right.toInt(),
+ currentNode.touchBoundsInRoot.bottom.toInt(),
+ )
val region = Region().also { it.set(boundsInRoot) }
val virtualViewId = if (currentNode.id == root.id) {
AccessibilityNodeProviderCompat.HOST_VIEW_ID
@@ -2836,7 +2851,12 @@
}
nodes[virtualViewId] = SemanticsNodeWithAdjustedBounds(
currentNode,
- boundsForFakeNode.toAndroidRect()
+ android.graphics.Rect(
+ boundsForFakeNode.left.toInt(),
+ boundsForFakeNode.top.toInt(),
+ boundsForFakeNode.right.toInt(),
+ boundsForFakeNode.bottom.toInt(),
+ )
)
} else if (virtualViewId == AccessibilityNodeProviderCompat.HOST_VIEW_ID) {
// Root view might have WRAP_CONTENT layout params in which case it will have zero
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutModifier.kt
index b66fc49..abb028f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutModifier.kt
@@ -16,15 +16,15 @@
package androidx.compose.ui.layout
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.GraphicsLayerScope
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.InspectorValueInfo
-import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.internal.JvmDefaultWithCompatibility
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.modifierElementOf
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.internal.JvmDefaultWithCompatibility
/**
* A [Modifier.Element] that changes how its wrapped content is measured and laid out.
@@ -263,37 +263,28 @@
*
* @see androidx.compose.ui.layout.LayoutModifier
*/
+@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.layout(
measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
-) = this.then(
- LayoutModifierImpl(
- measureBlock = measure,
- inspectorInfo = debugInspectorInfo {
- name = "layout"
- properties["measure"] = measure
- }
- )
+) = this then modifierElementOf(
+ key = measure,
+ create = { LayoutModifierImpl(measure) },
+ update = { layoutModifier -> layoutModifier.measureBlock = measure },
+ definitions = {
+ name = "layout"
+ properties["measure"] = measure
+ }
)
-private class LayoutModifierImpl(
- val measureBlock: MeasureScope.(Measurable, Constraints) -> MeasureResult,
- inspectorInfo: InspectorInfo.() -> Unit,
-) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
+@OptIn(ExperimentalComposeUiApi::class)
+internal class LayoutModifierImpl(
+ var measureBlock: MeasureScope.(Measurable, Constraints) -> MeasureResult
+) : LayoutModifierNode, Modifier.Node() {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
) = measureBlock(measurable, constraints)
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- val otherModifier = other as? LayoutModifierImpl ?: return false
- return measureBlock == otherModifier.measureBlock
- }
-
- override fun hashCode(): Int {
- return measureBlock.hashCode()
- }
-
override fun toString(): String {
return "LayoutModifierImpl(measureBlock=$measureBlock)"
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnPlacedModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnPlacedModifier.kt
index 619deb9..860638c0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnPlacedModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnPlacedModifier.kt
@@ -17,11 +17,11 @@
package androidx.compose.ui.layout
import androidx.compose.runtime.Stable
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.InspectorValueInfo
-import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.internal.JvmDefaultWithCompatibility
+import androidx.compose.ui.node.LayoutAwareModifierNode
+import androidx.compose.ui.node.modifierElementOf
/**
* Invoke [onPlaced] after the parent [LayoutModifier] and parent layout has been placed and before
@@ -30,38 +30,32 @@
*
* @sample androidx.compose.ui.samples.OnPlaced
*/
+@OptIn(ExperimentalComposeUiApi::class)
@Stable
fun Modifier.onPlaced(
onPlaced: (LayoutCoordinates) -> Unit
-) = this.then(
- OnPlacedModifierImpl(
- callback = onPlaced,
- inspectorInfo = debugInspectorInfo {
- name = "onPlaced"
- properties["onPlaced"] = onPlaced
- }
- )
+) = this then modifierElementOf(
+ key = onPlaced,
+ create = {
+ OnPlacedModifierImpl(callback = onPlaced)
+ },
+ update = {
+ it.callback = onPlaced
+ },
+ definitions = {
+ name = "onPlaced"
+ properties["onPlaced"] = onPlaced
+ }
)
+@OptIn(ExperimentalComposeUiApi::class)
private class OnPlacedModifierImpl(
- val callback: (LayoutCoordinates) -> Unit,
- inspectorInfo: InspectorInfo.() -> Unit
-) : OnPlacedModifier, InspectorValueInfo(inspectorInfo) {
+ var callback: (LayoutCoordinates) -> Unit
+) : LayoutAwareModifierNode, Modifier.Node() {
override fun onPlaced(coordinates: LayoutCoordinates) {
callback(coordinates)
}
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is OnPlacedModifierImpl) return false
-
- return callback == other.callback
- }
-
- override fun hashCode(): Int {
- return callback.hashCode()
- }
}
/**
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 2509402..1e0d0a9 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -45,6 +45,7 @@
import androidx.compose.ui.input.pointer.PointerInputModifier
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.LayoutModifierImpl
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
@@ -2317,45 +2318,49 @@
@Test
fun modifierMatchesWrapperWithIdentity() {
- val modifier1 = Modifier.layout { measurable, constraints ->
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) {
- placeable.place(0, 0)
+ val measureLambda1: MeasureScope.(Measurable, Constraints) -> MeasureResult =
+ { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
}
- }
- val modifier2 = Modifier.layout { measurable, constraints ->
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) {
- placeable.place(1, 1)
+ val modifier1 = Modifier.layout(measureLambda1)
+
+ val measureLambda2: MeasureScope.(Measurable, Constraints) -> MeasureResult =
+ { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(1, 1)
+ }
}
- }
+ val modifier2 = Modifier.layout(measureLambda2)
val root = LayoutNode()
root.modifier = modifier1.then(modifier2)
- val wrapper1 = root.outerCoordinator
- val wrapper2 = root.outerCoordinator.wrapped
-
assertEquals(
- modifier1,
- (wrapper1 as LayoutModifierNodeCoordinator).layoutModifierNode.toModifier()
+ measureLambda1,
+ ((root.outerCoordinator as LayoutModifierNodeCoordinator)
+ .layoutModifierNode as LayoutModifierImpl).measureBlock
)
assertEquals(
- modifier2,
- (wrapper2 as LayoutModifierNodeCoordinator).layoutModifierNode.toModifier()
+ measureLambda2,
+ ((root.outerCoordinator.wrapped as LayoutModifierNodeCoordinator)
+ .layoutModifierNode as LayoutModifierImpl).measureBlock
)
root.modifier = modifier2.then(modifier1)
assertEquals(
- modifier1,
- (root.outerCoordinator.wrapped as LayoutModifierNodeCoordinator)
- .layoutModifierNode
- .toModifier()
+ measureLambda1,
+ ((root.outerCoordinator.wrapped as LayoutModifierNodeCoordinator)
+ .layoutModifierNode as LayoutModifierImpl).measureBlock
)
assertEquals(
- modifier2,
- (root.outerCoordinator as LayoutModifierNodeCoordinator).layoutModifierNode.toModifier()
+ measureLambda2,
+ ((root.outerCoordinator as LayoutModifierNodeCoordinator)
+ .layoutModifierNode as LayoutModifierImpl).measureBlock
)
}
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
index 1c94351..c675c67 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
@@ -105,7 +105,6 @@
* ConstraintLayout.LayoutParams} for layout attributes
* </p>
*
- * <div class="special reference">
* <h3>Developer Guide</h3>
*
* <h4 id="RelativePositioning"> Relative positioning </h4>
@@ -556,7 +555,6 @@
* <p>This attribute is a mask, so you can decide to turn on or off
* specific optimizations by listing the ones you want.
* For example: <i>app:layout_optimizationLevel="direct|barrier|chain"</i> </p>
- * </div>
*/
public class ConstraintLayout extends ViewGroup {
/**
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index d61d7f9..498ff9f 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -18,8 +18,9 @@
docs(project(":ads:ads-identifier-testing"))
kmpDocs(project(":annotation:annotation"))
docs(project(":annotation:annotation-experimental"))
- docs(project(":appactions:interaction:interaction-proto"))
docs(project(":appactions:interaction:interaction-capabilities-core"))
+ docs(project(":appactions:interaction:interaction-proto"))
+ docs(project(":appactions:interaction:interaction-service"))
docs(project(":appcompat:appcompat"))
docs(project(":appcompat:appcompat-resources"))
docs(project(":appsearch:appsearch"))
diff --git a/fragment/OWNERS b/fragment/OWNERS
index 94b111e..b867087 100644
--- a/fragment/OWNERS
+++ b/fragment/OWNERS
@@ -1,4 +1,4 @@
-# Bug component: 460964
+# Bug component: 461227
ilake@google.com
jbwoods@google.com
mount@google.com
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 5c2b9ed..b751358 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -32,7 +32,7 @@
api("androidx.activity:activity:1.5.1")
api(projectOrArtifact(":lifecycle:lifecycle-runtime"))
api("androidx.lifecycle:lifecycle-livedata-core:2.5.1")
- api("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
api("androidx.savedstate:savedstate:1.2.0")
api("androidx.annotation:annotation-experimental:1.0.0")
@@ -57,9 +57,9 @@
androidTestImplementation(project(":internal-testutils-runtime"), {
exclude group: "androidx.fragment", module: "fragment"
})
- androidTestImplementation(project(":lifecycle:lifecycle-viewmodel"))
+ androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
- testImplementation(project(":fragment:fragment"))
+ testImplementation(projectOrArtifact(":fragment:fragment"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.testExtJunit)
testImplementation(libs.testCore)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ControllerHostCallbacks.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ControllerHostCallbacks.kt
index 095f013..f4ba719b 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ControllerHostCallbacks.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ControllerHostCallbacks.kt
@@ -81,12 +81,10 @@
class ControllerHostCallbacks(
private val activity: FragmentActivity,
- private val viewModelStore: ViewModelStore
+ private val vmStore: ViewModelStore
) : FragmentHostCallback<FragmentActivity>(activity), ViewModelStoreOwner {
- override fun getViewModelStore(): ViewModelStore {
- return viewModelStore
- }
+ override val viewModelStore: ViewModelStore = vmStore
override fun onDump(
prefix: String,
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ImageAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ImageAppWidget.kt
index f407767..1d16188 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ImageAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ImageAppWidget.kt
@@ -24,21 +24,27 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.glance.Button
+import androidx.glance.ColorFilter
import androidx.glance.GlanceModifier
import androidx.glance.Image
import androidx.glance.ImageProvider
+import androidx.glance.LocalContext
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.SizeMode
import androidx.glance.background
+import androidx.glance.color.ColorProvider
+import androidx.glance.layout.Alignment
import androidx.glance.layout.Column
import androidx.glance.layout.ContentScale
+import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.padding
import androidx.glance.layout.size
import androidx.glance.session.GlanceSessionManager
+import androidx.glance.text.Text
/**
* Sample AppWidget that showcase the [ContentScale] options for [Image]
@@ -52,6 +58,8 @@
override fun Content() {
var type by remember { mutableStateOf(ContentScale.Fit) }
Column(modifier = GlanceModifier.fillMaxSize().padding(8.dp)) {
+ Header()
+ Spacer(GlanceModifier.size(4.dp))
Button(
text = "Content Scale: ${type.asString()}",
modifier = GlanceModifier.fillMaxWidth(),
@@ -73,6 +81,28 @@
}
}
+ @Composable
+ private fun Header() {
+ val context = LocalContext.current
+ Row(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = GlanceModifier.fillMaxWidth().background(Color.White)
+ ) {
+ Image(
+ provider = ImageProvider(R.drawable.ic_android),
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(
+ ColorProvider(day = Color.Green, night = Color.Blue)
+ ),
+ )
+ Text(
+ text = context.getString(R.string.image_widget_name),
+ modifier = GlanceModifier.padding(8.dp),
+ )
+ }
+ }
+
private fun ContentScale.asString(): String =
when (this) {
ContentScale.Fit -> "Fit"
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/ic_android.xml b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/ic_android.xml
new file mode 100644
index 0000000..5508fb5
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/ic_android.xml
@@ -0,0 +1,5 @@
+<vector android:height="30dp"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="30dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M17.6,9.48l1.84,-3.18c0.16,-0.31 0.04,-0.69 -0.26,-0.85c-0.29,-0.15 -0.65,-0.06 -0.83,0.22l-1.88,3.24c-2.86,-1.21 -6.08,-1.21 -8.94,0L5.65,5.67c-0.19,-0.29 -0.58,-0.38 -0.87,-0.2C4.5,5.65 4.41,6.01 4.56,6.3L6.4,9.48C3.3,11.25 1.28,14.44 1,18h22C22.72,14.44 20.7,11.25 17.6,9.48zM7,15.25c-0.69,0 -1.25,-0.56 -1.25,-1.25c0,-0.69 0.56,-1.25 1.25,-1.25S8.25,13.31 8.25,14C8.25,14.69 7.69,15.25 7,15.25zM17,15.25c-0.69,0 -1.25,-0.56 -1.25,-1.25c0,-0.69 0.56,-1.25 1.25,-1.25s1.25,0.56 1.25,1.25C18.25,14.69 17.69,15.25 17,15.25z"/>
+</vector>
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/ImageTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/ImageTranslator.kt
index c3ddf74..f012ed0 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/ImageTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/ImageTranslator.kt
@@ -22,22 +22,32 @@
import android.widget.RemoteViews
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
import androidx.core.widget.RemoteViewsCompat.setImageViewAdjustViewBounds
+import androidx.core.widget.RemoteViewsCompat.setImageViewColorFilter
+import androidx.core.widget.RemoteViewsCompat.setImageViewColorFilterResource
import androidx.glance.AndroidResourceImageProvider
import androidx.glance.BitmapImageProvider
-import androidx.glance.layout.ContentScale
+import androidx.glance.ColorFilterParams
import androidx.glance.EmittableImage
import androidx.glance.IconImageProvider
+import androidx.glance.TintColorFilterParams
import androidx.glance.appwidget.GlanceAppWidgetTag
+import androidx.glance.appwidget.InsertedViewInfo
import androidx.glance.appwidget.LayoutType
import androidx.glance.appwidget.TranslationContext
import androidx.glance.appwidget.UriImageProvider
import androidx.glance.appwidget.applyModifiers
import androidx.glance.appwidget.insertView
+import androidx.glance.color.DayNightColorProvider
import androidx.glance.findModifier
+import androidx.glance.layout.ContentScale
import androidx.glance.layout.HeightModifier
import androidx.glance.layout.WidthModifier
+import androidx.glance.unit.ColorProvider
import androidx.glance.unit.Dimension
+import androidx.glance.unit.ResourceColorProvider
internal fun RemoteViews.translateEmittableImage(
translationContext: TranslationContext,
@@ -64,6 +74,7 @@
else ->
throw IllegalArgumentException("An unsupported ImageProvider type was used.")
}
+ element.colorFilterParams?.let { applyColorFilter(translationContext, this, it, viewDef) }
applyModifiers(translationContext, this, element.modifier, viewDef)
// If the content scale is Fit, the developer has expressed that they want the image to
@@ -76,6 +87,33 @@
setImageViewAdjustViewBounds(viewDef.mainViewId, shouldAdjustViewBounds)
}
+private fun applyColorFilter(
+ translationContext: TranslationContext,
+ rv: RemoteViews,
+ colorFilterParams: ColorFilterParams,
+ viewDef: InsertedViewInfo
+) {
+ when (colorFilterParams) {
+ is TintColorFilterParams -> {
+ val colorProvider = colorFilterParams.colorProvider
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ ImageTranslatorApi31Impl.applyTintColorFilter(
+ translationContext,
+ rv,
+ colorProvider,
+ viewDef.mainViewId
+ )
+ } else {
+ rv.setImageViewColorFilter(
+ viewDef.mainViewId, colorProvider.getColor(translationContext.context).toArgb()
+ )
+ }
+ }
+
+ else -> throw IllegalArgumentException("An unsupported ColorFilter was used.")
+ }
+}
+
private fun setImageViewIcon(rv: RemoteViews, viewId: Int, provider: IconImageProvider) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
throw IllegalStateException("Cannot use Icon ImageProvider before API 23.")
@@ -90,3 +128,37 @@
rv.setImageViewIcon(viewId, icon)
}
}
+
+@RequiresApi(Build.VERSION_CODES.S)
+private object ImageTranslatorApi31Impl {
+ @DoNotInline
+ fun applyTintColorFilter(
+ translationContext: TranslationContext,
+ rv: RemoteViews,
+ colorProvider: ColorProvider,
+ viewId: Int
+ ) {
+ when (colorProvider) {
+ is DayNightColorProvider -> rv.setImageViewColorFilter(
+ viewId,
+ colorProvider.day,
+ colorProvider.night
+ )
+
+ is ResourceColorProvider -> rv.setImageViewColorFilterResource(
+ viewId,
+ colorProvider.resId
+ )
+
+ else -> rv.setImageViewColorFilter(
+ viewId,
+ colorProvider.getColor(translationContext.context).toArgb()
+ )
+ }
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.S)
+internal fun RemoteViews.setImageViewColorFilter(viewId: Int, notNight: Color, night: Color) {
+ setImageViewColorFilter(viewId = viewId, notNight = notNight.toArgb(), night = night.toArgb())
+}
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/ImageTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/ImageTranslatorTest.kt
index 52bdc36..f459ca9 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/ImageTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/ImageTranslatorTest.kt
@@ -24,17 +24,22 @@
import android.graphics.drawable.Icon
import android.net.Uri
import android.widget.ImageView
+import androidx.compose.ui.graphics.Color
import androidx.core.graphics.drawable.toBitmap
+import androidx.glance.ColorFilter
import androidx.glance.GlanceModifier
-import androidx.glance.appwidget.applyRemoteViews
+import androidx.glance.Image
+import androidx.glance.ImageProvider
import androidx.glance.appwidget.ImageProvider
+import androidx.glance.appwidget.ImageViewSubject.Companion.assertThat
+import androidx.glance.appwidget.applyRemoteViews
import androidx.glance.appwidget.runAndTranslate
import androidx.glance.appwidget.test.R
import androidx.glance.layout.ContentScale
-import androidx.glance.Image
-import androidx.glance.ImageProvider
import androidx.glance.semantics.contentDescription
import androidx.glance.semantics.semantics
+import androidx.glance.unit.ColorProvider
+import androidx.glance.unit.ResourceColorProvider
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
@@ -220,4 +225,52 @@
val imageView = assertIs<ImageView>(context.applyRemoteViews(rv))
assertThat(imageView.getContentDescription()).isNull()
}
+
+ @Test
+ @Config(sdk = [23, 31])
+ fun translateImage_colorFilter() =
+ fakeCoroutineScope.runTest {
+ val rv = context.runAndTranslate {
+ Image(
+ provider = ImageProvider(R.drawable.oval),
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(ColorProvider(Color.Gray))
+ )
+ }
+
+ val imageView = assertIs<ImageView>(context.applyRemoteViews(rv))
+ assertThat(imageView).hasColorFilter(Color.Gray)
+ }
+
+ @Test
+ @Config(sdk = [23, 31])
+ fun translateImage_colorFilterWithResource() =
+ fakeCoroutineScope.runTest {
+ val colorProvider = ColorProvider(R.color.my_color) as ResourceColorProvider
+ val rv = context.runAndTranslate {
+ Image(
+ provider = ImageProvider(R.drawable.oval),
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(colorProvider)
+ )
+ }
+
+ val imageView = assertIs<ImageView>(context.applyRemoteViews(rv))
+ assertThat(imageView).hasColorFilter(colorProvider.getColor(context))
+ }
+
+ @Test
+ @Config(sdk = [23, 31])
+ fun translateImage_noColorFilter() =
+ fakeCoroutineScope.runTest {
+ val rv = context.runAndTranslate {
+ Image(
+ provider = ImageProvider(R.drawable.oval),
+ contentDescription = null,
+ )
+ }
+
+ val imageView = assertIs<ImageView>(context.applyRemoteViews(rv))
+ assertThat(imageView.colorFilter).isNull()
+ }
}
diff --git a/glance/glance-wear-tiles/build.gradle b/glance/glance-wear-tiles/build.gradle
index 3b48948..f1ac6fa 100644
--- a/glance/glance-wear-tiles/build.gradle
+++ b/glance/glance-wear-tiles/build.gradle
@@ -33,7 +33,7 @@
api("androidx.compose.runtime:runtime:1.1.1")
api("androidx.compose.ui:ui-graphics:1.1.1")
api("androidx.compose.ui:ui-unit:1.1.1")
- api("androidx.wear.tiles:tiles:1.0.0")
+ api("androidx.wear.tiles:tiles:1.1.0")
implementation(libs.kotlinStdlib)
implementation(libs.kotlinCoroutinesGuava)
diff --git a/glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/CountTileService.kt b/glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/CountTileService.kt
index 6f67ce1..b83d447 100644
--- a/glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/CountTileService.kt
+++ b/glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/CountTileService.kt
@@ -44,6 +44,7 @@
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
+import androidx.glance.unit.ColorProvider
import androidx.glance.wear.tiles.state.updateWearTileState
private val prefsCountKey = intPreferencesKey("count")
@@ -64,6 +65,7 @@
Text(
text = currentCount.toString(),
style = TextStyle(
+ color = ColorProvider(Color.Gray),
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)
diff --git a/glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/HelloTileService.kt b/glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/HelloTileService.kt
index 319c1bf0..bf46887 100644
--- a/glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/HelloTileService.kt
+++ b/glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/HelloTileService.kt
@@ -20,6 +20,7 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.glance.ColorFilter
import androidx.glance.GlanceModifier
import androidx.glance.Image
import androidx.glance.ImageProvider
@@ -36,6 +37,7 @@
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
+import androidx.glance.unit.ColorProvider
import androidx.glance.wear.tiles.GlanceTileService
class HelloTileService : GlanceTileService() {
@@ -50,15 +52,17 @@
verticalAlignment = Alignment.CenterVertically
) {
Image(
- provider = ImageProvider(R.mipmap.ic_launcher),
+ provider = ImageProvider(R.drawable.ic_waving_hand),
modifier = GlanceModifier.size(imageSize.width, imageSize.height),
- contentScale = ContentScale.FillBounds,
+ contentScale = ContentScale.Fit,
+ colorFilter = ColorFilter.tint(ColorProvider(Color.Yellow)),
contentDescription = "Hello tile icon"
)
Spacer(GlanceModifier.height(10.dp))
Text(
text = context.getString(R.string.hello_tile_greeting),
style = TextStyle(
+ color = ColorProvider(Color.White),
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)
diff --git a/glance/glance-wear-tiles/integration-tests/demos/src/main/res/drawable/ic_waving_hand.xml b/glance/glance-wear-tiles/integration-tests/demos/src/main/res/drawable/ic_waving_hand.xml
new file mode 100644
index 0000000..4ef53b4
--- /dev/null
+++ b/glance/glance-wear-tiles/integration-tests/demos/src/main/res/drawable/ic_waving_hand.xml
@@ -0,0 +1,20 @@
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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 android:height="24dp"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M23,17c0,3.31 -2.69,6 -6,6v-1.5c2.48,0 4.5,-2.02 4.5,-4.5H23zM1,7c0,-3.31 2.69,-6 6,-6v1.5C4.52,2.5 2.5,4.52 2.5,7H1zM8.01,4.32l-4.6,4.6c-3.22,3.22 -3.22,8.45 0,11.67s8.45,3.22 11.67,0l7.07,-7.07c0.49,-0.49 0.49,-1.28 0,-1.77c-0.49,-0.49 -1.28,-0.49 -1.77,0l-4.42,4.42l-0.71,-0.71l6.54,-6.54c0.49,-0.49 0.49,-1.28 0,-1.77s-1.28,-0.49 -1.77,0l-5.83,5.83l-0.71,-0.71l6.89,-6.89c0.49,-0.49 0.49,-1.28 0,-1.77s-1.28,-0.49 -1.77,0l-6.89,6.89L11.02,9.8l5.48,-5.48c0.49,-0.49 0.49,-1.28 0,-1.77s-1.28,-0.49 -1.77,0l-7.62,7.62c1.22,1.57 1.11,3.84 -0.33,5.28l-0.71,-0.71c1.17,-1.17 1.17,-3.07 0,-4.24l-0.35,-0.35l4.07,-4.07c0.49,-0.49 0.49,-1.28 0,-1.77C9.29,3.83 8.5,3.83 8.01,4.32z"/>
+</vector>
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
index 22b630f..71db58d 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
@@ -30,12 +30,11 @@
import androidx.glance.EmittableButton
import androidx.glance.EmittableImage
import androidx.glance.GlanceModifier
-import androidx.glance.semantics.SemanticsModifier
+import androidx.glance.TintColorFilterParams
import androidx.glance.VisibilityModifier
import androidx.glance.action.Action
import androidx.glance.action.ActionModifier
import androidx.glance.action.LambdaAction
-import androidx.glance.wear.tiles.action.RunCallbackAction
import androidx.glance.action.StartActivityAction
import androidx.glance.action.StartActivityClassAction
import androidx.glance.action.StartActivityComponentAction
@@ -51,6 +50,7 @@
import androidx.glance.layout.PaddingModifier
import androidx.glance.layout.WidthModifier
import androidx.glance.layout.collectPaddingInDp
+import androidx.glance.semantics.SemanticsModifier
import androidx.glance.semantics.SemanticsProperties
import androidx.glance.text.EmittableText
import androidx.glance.text.FontStyle
@@ -61,8 +61,9 @@
import androidx.glance.toEmittableText
import androidx.glance.unit.ColorProvider
import androidx.glance.unit.Dimension
-import androidx.glance.wear.tiles.curved.AnchorType
+import androidx.glance.wear.tiles.action.RunCallbackAction
import androidx.glance.wear.tiles.curved.ActionCurvedModifier
+import androidx.glance.wear.tiles.curved.AnchorType
import androidx.glance.wear.tiles.curved.CurvedTextStyle
import androidx.glance.wear.tiles.curved.EmittableCurvedChild
import androidx.glance.wear.tiles.curved.EmittableCurvedLine
@@ -573,6 +574,19 @@
}
)
+ element.colorFilterParams?.let { colorFilterParams ->
+ when (colorFilterParams) {
+ is TintColorFilterParams -> {
+ imageBuilder.setColorFilter(
+ LayoutElementBuilders.ColorFilter.Builder()
+ .setTint(argb(colorFilterParams.colorProvider.getColorAsArgb(context)))
+ .build()
+ )
+ }
+
+ else -> throw IllegalArgumentException("An unsupported ColorFilter was used.")
+ }
+ }
return imageBuilder.build()
}
diff --git a/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/WearCompositionTranslatorTest.kt b/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/WearCompositionTranslatorTest.kt
index 8b47c36..b8d34c6 100644
--- a/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/WearCompositionTranslatorTest.kt
+++ b/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/WearCompositionTranslatorTest.kt
@@ -27,6 +27,7 @@
import androidx.core.graphics.drawable.toBitmap
import androidx.glance.Button
import androidx.glance.ButtonColors
+import androidx.glance.ColorFilter
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.Image
@@ -90,6 +91,7 @@
import java.io.ByteArrayOutputStream
import java.util.Arrays
import kotlin.test.assertIs
+import kotlin.test.assertNotNull
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
@@ -759,6 +761,58 @@
}
@Test
+ fun translateImage_noColorFilter() = fakeCoroutineScope.runTest {
+ val compositionResult = runAndTranslate {
+ Image(
+ provider = ImageProvider(R.drawable.oval),
+ contentDescription = null,
+ modifier = GlanceModifier.width(R.dimen.dimension1).height(R.dimen.dimension2),
+ )
+ }
+
+ val content = compositionResult.layout
+ val image = (content as LayoutElementBuilders.Box).contents[0] as
+ LayoutElementBuilders.Image
+ assertThat(image.colorFilter).isNull()
+ }
+
+ @Test
+ fun translateImage_colorFilter() = fakeCoroutineScope.runTest {
+ val compositionResult = runAndTranslate {
+ Image(
+ provider = ImageProvider(R.drawable.oval),
+ contentDescription = null,
+ modifier = GlanceModifier.width(R.dimen.dimension1).height(R.dimen.dimension2),
+ colorFilter = ColorFilter.tint(ColorProvider(Color.Gray))
+ )
+ }
+
+ val content = compositionResult.layout
+ val image = (content as LayoutElementBuilders.Box).contents[0] as
+ LayoutElementBuilders.Image
+ val tint = assertNotNull(image.colorFilter?.tint)
+ assertThat(tint.argb).isEqualTo(Color.Gray.toArgb())
+ }
+
+ @Test
+ fun translateImage_colorFilterWithResource() = fakeCoroutineScope.runTest {
+ val compositionResult = runAndTranslate {
+ Image(
+ provider = ImageProvider(R.drawable.oval),
+ contentDescription = null,
+ modifier = GlanceModifier.width(R.dimen.dimension1).height(R.dimen.dimension2),
+ colorFilter = ColorFilter.tint(ColorProvider(R.color.color1))
+ )
+ }
+
+ val content = compositionResult.layout
+ val image = (content as LayoutElementBuilders.Box).contents[0] as
+ LayoutElementBuilders.Image
+ val tint = assertNotNull(image.colorFilter?.tint)
+ assertThat(tint.argb).isEqualTo(android.graphics.Color.rgb(0xC0, 0xFF, 0xEE))
+ }
+
+ @Test
fun setSizeFromResource() = fakeCoroutineScope.runTest {
val content = runAndTranslate {
Column(
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index 4d59f01..e92fd04 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -21,6 +21,14 @@
method @androidx.compose.runtime.Composable public static void Button(String text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.text.TextStyle? style, optional androidx.glance.ButtonColors colors, optional int maxLines);
}
+ public final class ColorFilter {
+ field public static final androidx.glance.ColorFilter.Companion Companion;
+ }
+
+ public static final class ColorFilter.Companion {
+ method public androidx.glance.ColorFilter tint(androidx.glance.unit.ColorProvider colorProvider);
+ }
+
public final class CombinedGlanceModifier implements androidx.glance.GlanceModifier {
ctor public CombinedGlanceModifier(androidx.glance.GlanceModifier outer, androidx.glance.GlanceModifier inner);
method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
@@ -85,7 +93,7 @@
}
public final class ImageKt {
- method @androidx.compose.runtime.Composable public static void Image(androidx.glance.ImageProvider provider, String? contentDescription, optional androidx.glance.GlanceModifier modifier, optional int contentScale);
+ method @androidx.compose.runtime.Composable public static void Image(androidx.glance.ImageProvider provider, String? contentDescription, optional androidx.glance.GlanceModifier modifier, optional int contentScale, optional androidx.glance.ColorFilter? colorFilter);
method public static androidx.glance.ImageProvider ImageProvider(@DrawableRes int resId);
method public static androidx.glance.ImageProvider ImageProvider(android.graphics.Bitmap bitmap);
method @RequiresApi(android.os.Build.VERSION_CODES.M) public static androidx.glance.ImageProvider ImageProvider(android.graphics.drawable.Icon icon);
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index 4d59f01..e92fd04 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -21,6 +21,14 @@
method @androidx.compose.runtime.Composable public static void Button(String text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.text.TextStyle? style, optional androidx.glance.ButtonColors colors, optional int maxLines);
}
+ public final class ColorFilter {
+ field public static final androidx.glance.ColorFilter.Companion Companion;
+ }
+
+ public static final class ColorFilter.Companion {
+ method public androidx.glance.ColorFilter tint(androidx.glance.unit.ColorProvider colorProvider);
+ }
+
public final class CombinedGlanceModifier implements androidx.glance.GlanceModifier {
ctor public CombinedGlanceModifier(androidx.glance.GlanceModifier outer, androidx.glance.GlanceModifier inner);
method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
@@ -85,7 +93,7 @@
}
public final class ImageKt {
- method @androidx.compose.runtime.Composable public static void Image(androidx.glance.ImageProvider provider, String? contentDescription, optional androidx.glance.GlanceModifier modifier, optional int contentScale);
+ method @androidx.compose.runtime.Composable public static void Image(androidx.glance.ImageProvider provider, String? contentDescription, optional androidx.glance.GlanceModifier modifier, optional int contentScale, optional androidx.glance.ColorFilter? colorFilter);
method public static androidx.glance.ImageProvider ImageProvider(@DrawableRes int resId);
method public static androidx.glance.ImageProvider ImageProvider(android.graphics.Bitmap bitmap);
method @RequiresApi(android.os.Build.VERSION_CODES.M) public static androidx.glance.ImageProvider ImageProvider(android.graphics.drawable.Icon icon);
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index 4d59f01..e92fd04 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -21,6 +21,14 @@
method @androidx.compose.runtime.Composable public static void Button(String text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.text.TextStyle? style, optional androidx.glance.ButtonColors colors, optional int maxLines);
}
+ public final class ColorFilter {
+ field public static final androidx.glance.ColorFilter.Companion Companion;
+ }
+
+ public static final class ColorFilter.Companion {
+ method public androidx.glance.ColorFilter tint(androidx.glance.unit.ColorProvider colorProvider);
+ }
+
public final class CombinedGlanceModifier implements androidx.glance.GlanceModifier {
ctor public CombinedGlanceModifier(androidx.glance.GlanceModifier outer, androidx.glance.GlanceModifier inner);
method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
@@ -85,7 +93,7 @@
}
public final class ImageKt {
- method @androidx.compose.runtime.Composable public static void Image(androidx.glance.ImageProvider provider, String? contentDescription, optional androidx.glance.GlanceModifier modifier, optional int contentScale);
+ method @androidx.compose.runtime.Composable public static void Image(androidx.glance.ImageProvider provider, String? contentDescription, optional androidx.glance.GlanceModifier modifier, optional int contentScale, optional androidx.glance.ColorFilter? colorFilter);
method public static androidx.glance.ImageProvider ImageProvider(@DrawableRes int resId);
method public static androidx.glance.ImageProvider ImageProvider(android.graphics.Bitmap bitmap);
method @RequiresApi(android.os.Build.VERSION_CODES.M) public static androidx.glance.ImageProvider ImageProvider(android.graphics.drawable.Icon icon);
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/Image.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/Image.kt
index 7869e12..440722f 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/Image.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/Image.kt
@@ -26,6 +26,7 @@
import androidx.glance.layout.ContentScale
import androidx.glance.semantics.contentDescription
import androidx.glance.semantics.semantics
+import androidx.glance.unit.ColorProvider
/**
* Interface representing an Image source which can be used with a Glance [Image] element.
@@ -76,21 +77,49 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
/** @suppress */
+interface ColorFilterParams
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+/** @suppress */
+class TintColorFilterParams(val colorProvider: ColorProvider) : ColorFilterParams {
+ override fun toString() =
+ "TintColorFilterParams(colorProvider=$colorProvider))"
+}
+
+/**
+ * Effects used to modify the color of an image.
+ */
+class ColorFilter internal constructor(internal val colorFilterParams: ColorFilterParams) {
+ companion object {
+ /**
+ * Set a tinting option for the image using the platform-specific default blending mode.
+ *
+ * @param colorProvider Provider used to get the color for blending the source content.
+ */
+ fun tint(colorProvider: ColorProvider): ColorFilter =
+ ColorFilter(TintColorFilterParams(colorProvider))
+ }
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+/** @suppress */
class EmittableImage : Emittable {
override var modifier: GlanceModifier = GlanceModifier
-
var provider: ImageProvider? = null
+ var colorFilterParams: ColorFilterParams? = null
var contentScale: ContentScale = ContentScale.Fit
override fun copy(): Emittable = EmittableImage().also {
it.modifier = modifier
it.provider = provider
+ it.colorFilterParams = colorFilterParams
it.contentScale = contentScale
}
override fun toString(): String = "EmittableImage(" +
"modifier=$modifier, " +
"provider=$provider, " +
+ "colorFilterParams=$colorFilterParams, " +
"contentScale=$contentScale" +
")"
}
@@ -108,13 +137,15 @@
* @param modifier Modifier used to adjust the layout algorithm or draw decoration content.
* @param contentScale How to lay the image out with respect to its bounds, if the bounds are
* smaller than the image.
+ * @param colorFilter The effects to use to modify the color of an image.
*/
@Composable
fun Image(
provider: ImageProvider,
contentDescription: String?,
modifier: GlanceModifier = GlanceModifier,
- contentScale: ContentScale = ContentScale.Fit
+ contentScale: ContentScale = ContentScale.Fit,
+ colorFilter: ColorFilter? = null
) {
val finalModifier = if (contentDescription != null) {
modifier.semantics {
@@ -130,6 +161,7 @@
this.set(provider) { this.provider = it }
this.set(finalModifier) { this.modifier = it }
this.set(contentScale) { this.contentScale = it }
+ this.set(colorFilter) { this.colorFilterParams = it?.colorFilterParams }
}
)
}
diff --git a/glance/glance/src/test/kotlin/androidx/glance/ImageTest.kt b/glance/glance/src/test/kotlin/androidx/glance/ImageTest.kt
index 38c08f1..95473a6 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/ImageTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/ImageTest.kt
@@ -16,6 +16,7 @@
package androidx.glance
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.glance.layout.ContentScale
import androidx.glance.layout.PaddingModifier
@@ -23,6 +24,7 @@
import androidx.glance.layout.runTestingComposition
import androidx.glance.semantics.SemanticsModifier
import androidx.glance.semantics.SemanticsProperties
+import androidx.glance.unit.ColorProvider
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
@@ -64,5 +66,29 @@
.containsExactly("Hello World")
assertThat(img.contentScale).isEqualTo(ContentScale.FillBounds)
assertThat(img.modifier.findModifier<PaddingModifier>()).isNotNull()
+ assertThat(img.colorFilterParams).isNull()
+ }
+
+ @Test
+ fun createImage_tintColorFilter() {
+ val colorProvider = ColorProvider(Color.Gray)
+ fakeCoroutineScope.runTest {
+ val root = runTestingComposition {
+ Image(
+ provider = ImageProvider(5),
+ contentDescription = "Hello World",
+ modifier = GlanceModifier.padding(5.dp),
+ colorFilter = ColorFilter.tint(colorProvider)
+ )
+ }
+
+ assertThat(root.children).hasSize(1)
+ assertThat(root.children[0]).isInstanceOf(EmittableImage::class.java)
+
+ val img = root.children[0] as EmittableImage
+
+ val colorFilterParams = assertIs<TintColorFilterParams>(img.colorFilterParams)
+ assertThat(colorFilterParams.colorProvider).isEqualTo(colorProvider)
+ }
}
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 2f23eb1..2d02b1b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -58,6 +58,7 @@
wire = "4.4.1"
[libraries]
+agpTestingPlatformCoreProto = { module = "com.google.testing.platform:core-proto", version = "0.0.8-alpha08" }
androidAccessibilityFramework = { module = "com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework", version = { strictly = "2.1" } }
androidBuilderModelMin = { module = "com.android.tools.build:builder-model", version.ref = "androidGradlePluginMin" }
androidGradlePluginz = { module = "com.android.tools.build:gradle", version.ref = "androidGradlePlugin" }
diff --git a/graphics/graphics-core/api/current.txt b/graphics/graphics-core/api/current.txt
index 4b82337..92d9c70 100644
--- a/graphics/graphics-core/api/current.txt
+++ b/graphics/graphics-core/api/current.txt
@@ -1,6 +1,52 @@
// Signature format: 4.0
package androidx.graphics.lowlatency {
+ public final class BufferInfo {
+ method public int getFrameBufferId();
+ method public int getHeight();
+ method public int getWidth();
+ property public final int frameBufferId;
+ property public final int height;
+ property public final int width;
+ }
+
+ public final class FrontBufferSyncStrategy implements androidx.graphics.opengl.SyncStrategy {
+ ctor public FrontBufferSyncStrategy(long usageFlags);
+ method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
+ method public boolean isVisible();
+ method public void setVisible(boolean);
+ property public final boolean isVisible;
+ }
+
+ @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class GLFrontBufferedRenderer<T> {
+ ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback, optional androidx.graphics.opengl.GLRenderer? glRenderer);
+ ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback);
+ method public void cancel();
+ method public void clear();
+ method public void commit();
+ method public void execute(Runnable runnable);
+ method public boolean isValid();
+ method public void release(boolean cancelPending, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onReleaseComplete);
+ method public void release(boolean cancelPending);
+ method public void renderDoubleBufferedLayer(java.util.Collection<? extends T> params);
+ method public void renderFrontBufferedLayer(T? param);
+ field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
+ }
+
+ @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
+ method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
+ method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, java.util.Collection<? extends T> params);
+ method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, T? param);
+ method @WorkerThread public default void onFrontBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
+ }
+
+ public static final class GLFrontBufferedRenderer.Companion {
+ }
+
+}
+
+package androidx.graphics.opengl {
+
@RequiresApi(android.os.Build.VERSION_CODES.O) public final class FrameBuffer implements java.lang.AutoCloseable {
ctor public FrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl, android.hardware.HardwareBuffer hardwareBuffer);
method public void close();
@@ -12,81 +58,17 @@
}
@RequiresApi(android.os.Build.VERSION_CODES.O) public final class FrameBufferRenderer implements androidx.graphics.opengl.GLRenderer.RenderCallback {
- ctor public FrameBufferRenderer(androidx.graphics.lowlatency.FrameBufferRenderer.RenderCallback frameBufferRendererCallbacks, optional androidx.graphics.lowlatency.SyncStrategy syncStrategy);
+ ctor public FrameBufferRenderer(androidx.graphics.opengl.FrameBufferRenderer.RenderCallback frameBufferRendererCallbacks, optional androidx.graphics.opengl.SyncStrategy syncStrategy);
method public void clear();
method public void onDrawFrame(androidx.graphics.opengl.egl.EGLManager eglManager);
}
public static interface FrameBufferRenderer.RenderCallback {
- method public androidx.graphics.lowlatency.FrameBuffer obtainFrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl);
+ method public androidx.graphics.opengl.FrameBuffer obtainFrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl);
method public void onDraw(androidx.graphics.opengl.egl.EGLManager eglManager);
- method public void onDrawComplete(androidx.graphics.lowlatency.FrameBuffer frameBuffer, androidx.graphics.lowlatency.SyncFenceCompat? syncFenceCompat);
+ method public void onDrawComplete(androidx.graphics.opengl.FrameBuffer frameBuffer, androidx.hardware.SyncFenceCompat? syncFenceCompat);
}
- public final class FrontBufferSyncStrategy implements androidx.graphics.lowlatency.SyncStrategy {
- ctor public FrontBufferSyncStrategy(long usageFlags);
- method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.graphics.lowlatency.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
- method public boolean isVisible();
- method public void setVisible(boolean);
- property public final boolean isVisible;
- }
-
- @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class GLFrontBufferedRenderer<T> {
- ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback, optional androidx.graphics.opengl.GLRenderer? glRenderer);
- ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback);
- method public void clear();
- method public void commit();
- method public boolean isValid();
- method public void release(boolean cancelPending, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onReleaseComplete);
- method public void release(boolean cancelPending);
- method public void renderDoubleBufferedLayer(java.util.Collection<? extends T> params);
- method public void renderFrontBufferedLayer(T? param);
- field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
- }
-
- @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
- method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
- method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, java.util.Collection<? extends T> params);
- method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, T? param);
- method @WorkerThread public default void onFrontBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
- }
-
- public static final class GLFrontBufferedRenderer.Companion {
- }
-
- @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFenceCompat implements java.lang.AutoCloseable {
- method public boolean await(long timeoutNanos);
- method public boolean awaitForever();
- method public void close();
- method public static androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec egl);
- method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTimeNanos();
- method public boolean isValid();
- field public static final androidx.graphics.lowlatency.SyncFenceCompat.Companion Companion;
- field public static final long SIGNAL_TIME_INVALID = -1L; // 0xffffffffffffffffL
- field public static final long SIGNAL_TIME_PENDING = 9223372036854775807L; // 0x7fffffffffffffffL
- }
-
- public static final class SyncFenceCompat.Companion {
- method public androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec egl);
- }
-
- public final class SyncFenceCompatKt {
- method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) @kotlin.jvm.JvmSynthetic public static androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec);
- }
-
- public interface SyncStrategy {
- method public androidx.graphics.lowlatency.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
- field public static final androidx.graphics.lowlatency.SyncStrategy ALWAYS;
- field public static final androidx.graphics.lowlatency.SyncStrategy.Companion Companion;
- }
-
- public static final class SyncStrategy.Companion {
- }
-
-}
-
-package androidx.graphics.opengl {
-
public final class GLRenderer {
ctor public GLRenderer(optional kotlin.jvm.functions.Function0<? extends androidx.graphics.opengl.egl.EGLSpec> eglSpecFactory, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EGLManager,? extends android.opengl.EGLConfig> eglConfigFactory);
method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.Surface surface, int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
@@ -95,6 +77,7 @@
method public androidx.graphics.opengl.GLRenderer.RenderTarget createRenderTarget(int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending);
+ method public void execute(Runnable runnable);
method public boolean isRunning();
method public void registerEGLContextCallback(androidx.graphics.opengl.GLRenderer.EGLContextCallback callback);
method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
@@ -132,6 +115,15 @@
method public void resize(int width, int height);
}
+ public interface SyncStrategy {
+ method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
+ field public static final androidx.graphics.opengl.SyncStrategy ALWAYS;
+ field public static final androidx.graphics.opengl.SyncStrategy.Companion Companion;
+ }
+
+ public static final class SyncStrategy.Companion {
+ }
+
}
package androidx.graphics.opengl.egl {
@@ -211,7 +203,6 @@
method public boolean eglDestroyImageKHR(androidx.opengl.EGLImageKHR image);
method public boolean eglDestroySurface(android.opengl.EGLSurface surface);
method public boolean eglDestroySyncKHR(androidx.opengl.EGLSyncKHR sync);
- method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(androidx.opengl.EGLSyncKHR sync);
method public android.opengl.EGLSurface eglGetCurrentDrawSurface();
method public android.opengl.EGLSurface eglGetCurrentReadSurface();
method public int eglGetError();
@@ -286,8 +277,8 @@
method public androidx.graphics.surface.SurfaceControlCompat.Transaction reparent(androidx.graphics.surface.SurfaceControlCompat surfaceControl, androidx.graphics.surface.SurfaceControlCompat? newParent);
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public androidx.graphics.surface.SurfaceControlCompat.Transaction reparent(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.view.AttachedSurfaceControl attachedSurfaceControl);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setAlpha(androidx.graphics.surface.SurfaceControlCompat surfaceControl, float alpha);
- method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.graphics.lowlatency.SyncFenceCompat? fence, optional kotlin.jvm.functions.Function0<kotlin.Unit>? releaseCallback);
- method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.graphics.lowlatency.SyncFenceCompat? fence);
+ method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.hardware.SyncFenceCompat? fence, optional kotlin.jvm.functions.Function0<kotlin.Unit>? releaseCallback);
+ method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.hardware.SyncFenceCompat? fence);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer);
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);
@@ -307,20 +298,20 @@
package androidx.hardware {
- @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFence implements java.lang.AutoCloseable {
- ctor public SyncFence(int fd);
+ @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFenceCompat implements java.lang.AutoCloseable {
method public boolean await(long timeoutNanos);
method public boolean awaitForever();
method public void close();
- method protected void finalize();
- method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTime();
+ method public static androidx.hardware.SyncFenceCompat createNativeSyncFence();
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTimeNanos();
method public boolean isValid();
- field public static final androidx.hardware.SyncFence.Companion Companion;
+ field public static final androidx.hardware.SyncFenceCompat.Companion Companion;
field public static final long SIGNAL_TIME_INVALID = -1L; // 0xffffffffffffffffL
field public static final long SIGNAL_TIME_PENDING = 9223372036854775807L; // 0x7fffffffffffffffL
}
- public static final class SyncFence.Companion {
+ public static final class SyncFenceCompat.Companion {
+ method public androidx.hardware.SyncFenceCompat createNativeSyncFence();
}
}
@@ -333,7 +324,6 @@
method public static androidx.opengl.EGLSyncKHR? eglCreateSyncKHR(android.opengl.EGLDisplay eglDisplay, int type, androidx.graphics.opengl.egl.EGLConfigAttributes? attributes);
method public static boolean eglDestroyImageKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLImageKHR image);
method public static boolean eglDestroySyncKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR eglSync);
- method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public static androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(android.opengl.EGLDisplay display, androidx.opengl.EGLSyncKHR sync);
method public static boolean eglGetSyncAttribKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR sync, int attribute, int[] value, int offset);
method public static void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
method public static java.util.Set<java.lang.String> parseExtensions(String queryString);
@@ -378,7 +368,6 @@
method public androidx.opengl.EGLSyncKHR? eglCreateSyncKHR(android.opengl.EGLDisplay eglDisplay, int type, androidx.graphics.opengl.egl.EGLConfigAttributes? attributes);
method public boolean eglDestroyImageKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLImageKHR image);
method public boolean eglDestroySyncKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR eglSync);
- method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(android.opengl.EGLDisplay display, androidx.opengl.EGLSyncKHR sync);
method public boolean eglGetSyncAttribKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR sync, int attribute, int[] value, int offset);
method public void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
method public java.util.Set<java.lang.String> parseExtensions(String queryString);
diff --git a/graphics/graphics-core/api/public_plus_experimental_current.txt b/graphics/graphics-core/api/public_plus_experimental_current.txt
index 4b82337..92d9c70 100644
--- a/graphics/graphics-core/api/public_plus_experimental_current.txt
+++ b/graphics/graphics-core/api/public_plus_experimental_current.txt
@@ -1,6 +1,52 @@
// Signature format: 4.0
package androidx.graphics.lowlatency {
+ public final class BufferInfo {
+ method public int getFrameBufferId();
+ method public int getHeight();
+ method public int getWidth();
+ property public final int frameBufferId;
+ property public final int height;
+ property public final int width;
+ }
+
+ public final class FrontBufferSyncStrategy implements androidx.graphics.opengl.SyncStrategy {
+ ctor public FrontBufferSyncStrategy(long usageFlags);
+ method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
+ method public boolean isVisible();
+ method public void setVisible(boolean);
+ property public final boolean isVisible;
+ }
+
+ @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class GLFrontBufferedRenderer<T> {
+ ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback, optional androidx.graphics.opengl.GLRenderer? glRenderer);
+ ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback);
+ method public void cancel();
+ method public void clear();
+ method public void commit();
+ method public void execute(Runnable runnable);
+ method public boolean isValid();
+ method public void release(boolean cancelPending, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onReleaseComplete);
+ method public void release(boolean cancelPending);
+ method public void renderDoubleBufferedLayer(java.util.Collection<? extends T> params);
+ method public void renderFrontBufferedLayer(T? param);
+ field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
+ }
+
+ @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
+ method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
+ method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, java.util.Collection<? extends T> params);
+ method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, T? param);
+ method @WorkerThread public default void onFrontBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
+ }
+
+ public static final class GLFrontBufferedRenderer.Companion {
+ }
+
+}
+
+package androidx.graphics.opengl {
+
@RequiresApi(android.os.Build.VERSION_CODES.O) public final class FrameBuffer implements java.lang.AutoCloseable {
ctor public FrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl, android.hardware.HardwareBuffer hardwareBuffer);
method public void close();
@@ -12,81 +58,17 @@
}
@RequiresApi(android.os.Build.VERSION_CODES.O) public final class FrameBufferRenderer implements androidx.graphics.opengl.GLRenderer.RenderCallback {
- ctor public FrameBufferRenderer(androidx.graphics.lowlatency.FrameBufferRenderer.RenderCallback frameBufferRendererCallbacks, optional androidx.graphics.lowlatency.SyncStrategy syncStrategy);
+ ctor public FrameBufferRenderer(androidx.graphics.opengl.FrameBufferRenderer.RenderCallback frameBufferRendererCallbacks, optional androidx.graphics.opengl.SyncStrategy syncStrategy);
method public void clear();
method public void onDrawFrame(androidx.graphics.opengl.egl.EGLManager eglManager);
}
public static interface FrameBufferRenderer.RenderCallback {
- method public androidx.graphics.lowlatency.FrameBuffer obtainFrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl);
+ method public androidx.graphics.opengl.FrameBuffer obtainFrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl);
method public void onDraw(androidx.graphics.opengl.egl.EGLManager eglManager);
- method public void onDrawComplete(androidx.graphics.lowlatency.FrameBuffer frameBuffer, androidx.graphics.lowlatency.SyncFenceCompat? syncFenceCompat);
+ method public void onDrawComplete(androidx.graphics.opengl.FrameBuffer frameBuffer, androidx.hardware.SyncFenceCompat? syncFenceCompat);
}
- public final class FrontBufferSyncStrategy implements androidx.graphics.lowlatency.SyncStrategy {
- ctor public FrontBufferSyncStrategy(long usageFlags);
- method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.graphics.lowlatency.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
- method public boolean isVisible();
- method public void setVisible(boolean);
- property public final boolean isVisible;
- }
-
- @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class GLFrontBufferedRenderer<T> {
- ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback, optional androidx.graphics.opengl.GLRenderer? glRenderer);
- ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback);
- method public void clear();
- method public void commit();
- method public boolean isValid();
- method public void release(boolean cancelPending, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onReleaseComplete);
- method public void release(boolean cancelPending);
- method public void renderDoubleBufferedLayer(java.util.Collection<? extends T> params);
- method public void renderFrontBufferedLayer(T? param);
- field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
- }
-
- @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
- method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
- method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, java.util.Collection<? extends T> params);
- method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, T? param);
- method @WorkerThread public default void onFrontBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
- }
-
- public static final class GLFrontBufferedRenderer.Companion {
- }
-
- @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFenceCompat implements java.lang.AutoCloseable {
- method public boolean await(long timeoutNanos);
- method public boolean awaitForever();
- method public void close();
- method public static androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec egl);
- method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTimeNanos();
- method public boolean isValid();
- field public static final androidx.graphics.lowlatency.SyncFenceCompat.Companion Companion;
- field public static final long SIGNAL_TIME_INVALID = -1L; // 0xffffffffffffffffL
- field public static final long SIGNAL_TIME_PENDING = 9223372036854775807L; // 0x7fffffffffffffffL
- }
-
- public static final class SyncFenceCompat.Companion {
- method public androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec egl);
- }
-
- public final class SyncFenceCompatKt {
- method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) @kotlin.jvm.JvmSynthetic public static androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec);
- }
-
- public interface SyncStrategy {
- method public androidx.graphics.lowlatency.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
- field public static final androidx.graphics.lowlatency.SyncStrategy ALWAYS;
- field public static final androidx.graphics.lowlatency.SyncStrategy.Companion Companion;
- }
-
- public static final class SyncStrategy.Companion {
- }
-
-}
-
-package androidx.graphics.opengl {
-
public final class GLRenderer {
ctor public GLRenderer(optional kotlin.jvm.functions.Function0<? extends androidx.graphics.opengl.egl.EGLSpec> eglSpecFactory, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EGLManager,? extends android.opengl.EGLConfig> eglConfigFactory);
method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.Surface surface, int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
@@ -95,6 +77,7 @@
method public androidx.graphics.opengl.GLRenderer.RenderTarget createRenderTarget(int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending);
+ method public void execute(Runnable runnable);
method public boolean isRunning();
method public void registerEGLContextCallback(androidx.graphics.opengl.GLRenderer.EGLContextCallback callback);
method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
@@ -132,6 +115,15 @@
method public void resize(int width, int height);
}
+ public interface SyncStrategy {
+ method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
+ field public static final androidx.graphics.opengl.SyncStrategy ALWAYS;
+ field public static final androidx.graphics.opengl.SyncStrategy.Companion Companion;
+ }
+
+ public static final class SyncStrategy.Companion {
+ }
+
}
package androidx.graphics.opengl.egl {
@@ -211,7 +203,6 @@
method public boolean eglDestroyImageKHR(androidx.opengl.EGLImageKHR image);
method public boolean eglDestroySurface(android.opengl.EGLSurface surface);
method public boolean eglDestroySyncKHR(androidx.opengl.EGLSyncKHR sync);
- method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(androidx.opengl.EGLSyncKHR sync);
method public android.opengl.EGLSurface eglGetCurrentDrawSurface();
method public android.opengl.EGLSurface eglGetCurrentReadSurface();
method public int eglGetError();
@@ -286,8 +277,8 @@
method public androidx.graphics.surface.SurfaceControlCompat.Transaction reparent(androidx.graphics.surface.SurfaceControlCompat surfaceControl, androidx.graphics.surface.SurfaceControlCompat? newParent);
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public androidx.graphics.surface.SurfaceControlCompat.Transaction reparent(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.view.AttachedSurfaceControl attachedSurfaceControl);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setAlpha(androidx.graphics.surface.SurfaceControlCompat surfaceControl, float alpha);
- method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.graphics.lowlatency.SyncFenceCompat? fence, optional kotlin.jvm.functions.Function0<kotlin.Unit>? releaseCallback);
- method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.graphics.lowlatency.SyncFenceCompat? fence);
+ method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.hardware.SyncFenceCompat? fence, optional kotlin.jvm.functions.Function0<kotlin.Unit>? releaseCallback);
+ method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.hardware.SyncFenceCompat? fence);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer);
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);
@@ -307,20 +298,20 @@
package androidx.hardware {
- @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFence implements java.lang.AutoCloseable {
- ctor public SyncFence(int fd);
+ @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFenceCompat implements java.lang.AutoCloseable {
method public boolean await(long timeoutNanos);
method public boolean awaitForever();
method public void close();
- method protected void finalize();
- method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTime();
+ method public static androidx.hardware.SyncFenceCompat createNativeSyncFence();
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTimeNanos();
method public boolean isValid();
- field public static final androidx.hardware.SyncFence.Companion Companion;
+ field public static final androidx.hardware.SyncFenceCompat.Companion Companion;
field public static final long SIGNAL_TIME_INVALID = -1L; // 0xffffffffffffffffL
field public static final long SIGNAL_TIME_PENDING = 9223372036854775807L; // 0x7fffffffffffffffL
}
- public static final class SyncFence.Companion {
+ public static final class SyncFenceCompat.Companion {
+ method public androidx.hardware.SyncFenceCompat createNativeSyncFence();
}
}
@@ -333,7 +324,6 @@
method public static androidx.opengl.EGLSyncKHR? eglCreateSyncKHR(android.opengl.EGLDisplay eglDisplay, int type, androidx.graphics.opengl.egl.EGLConfigAttributes? attributes);
method public static boolean eglDestroyImageKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLImageKHR image);
method public static boolean eglDestroySyncKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR eglSync);
- method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public static androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(android.opengl.EGLDisplay display, androidx.opengl.EGLSyncKHR sync);
method public static boolean eglGetSyncAttribKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR sync, int attribute, int[] value, int offset);
method public static void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
method public static java.util.Set<java.lang.String> parseExtensions(String queryString);
@@ -378,7 +368,6 @@
method public androidx.opengl.EGLSyncKHR? eglCreateSyncKHR(android.opengl.EGLDisplay eglDisplay, int type, androidx.graphics.opengl.egl.EGLConfigAttributes? attributes);
method public boolean eglDestroyImageKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLImageKHR image);
method public boolean eglDestroySyncKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR eglSync);
- method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(android.opengl.EGLDisplay display, androidx.opengl.EGLSyncKHR sync);
method public boolean eglGetSyncAttribKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR sync, int attribute, int[] value, int offset);
method public void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
method public java.util.Set<java.lang.String> parseExtensions(String queryString);
diff --git a/graphics/graphics-core/api/restricted_current.txt b/graphics/graphics-core/api/restricted_current.txt
index 583dc42..47f6f95 100644
--- a/graphics/graphics-core/api/restricted_current.txt
+++ b/graphics/graphics-core/api/restricted_current.txt
@@ -1,6 +1,52 @@
// Signature format: 4.0
package androidx.graphics.lowlatency {
+ public final class BufferInfo {
+ method public int getFrameBufferId();
+ method public int getHeight();
+ method public int getWidth();
+ property public final int frameBufferId;
+ property public final int height;
+ property public final int width;
+ }
+
+ public final class FrontBufferSyncStrategy implements androidx.graphics.opengl.SyncStrategy {
+ ctor public FrontBufferSyncStrategy(long usageFlags);
+ method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
+ method public boolean isVisible();
+ method public void setVisible(boolean);
+ property public final boolean isVisible;
+ }
+
+ @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class GLFrontBufferedRenderer<T> {
+ ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback, optional androidx.graphics.opengl.GLRenderer? glRenderer);
+ ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback);
+ method public void cancel();
+ method public void clear();
+ method public void commit();
+ method public void execute(Runnable runnable);
+ method public boolean isValid();
+ method public void release(boolean cancelPending, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onReleaseComplete);
+ method public void release(boolean cancelPending);
+ method public void renderDoubleBufferedLayer(java.util.Collection<? extends T> params);
+ method public void renderFrontBufferedLayer(T? param);
+ field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
+ }
+
+ @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
+ method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
+ method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, java.util.Collection<? extends T> params);
+ method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, androidx.graphics.lowlatency.BufferInfo bufferInfo, float[] transform, T? param);
+ method @WorkerThread public default void onFrontBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
+ }
+
+ public static final class GLFrontBufferedRenderer.Companion {
+ }
+
+}
+
+package androidx.graphics.opengl {
+
@RequiresApi(android.os.Build.VERSION_CODES.O) public final class FrameBuffer implements java.lang.AutoCloseable {
ctor public FrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl, android.hardware.HardwareBuffer hardwareBuffer);
method public void close();
@@ -12,81 +58,17 @@
}
@RequiresApi(android.os.Build.VERSION_CODES.O) public final class FrameBufferRenderer implements androidx.graphics.opengl.GLRenderer.RenderCallback {
- ctor public FrameBufferRenderer(androidx.graphics.lowlatency.FrameBufferRenderer.RenderCallback frameBufferRendererCallbacks, optional androidx.graphics.lowlatency.SyncStrategy syncStrategy);
+ ctor public FrameBufferRenderer(androidx.graphics.opengl.FrameBufferRenderer.RenderCallback frameBufferRendererCallbacks, optional androidx.graphics.opengl.SyncStrategy syncStrategy);
method public void clear();
method public void onDrawFrame(androidx.graphics.opengl.egl.EGLManager eglManager);
}
public static interface FrameBufferRenderer.RenderCallback {
- method public androidx.graphics.lowlatency.FrameBuffer obtainFrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl);
+ method public androidx.graphics.opengl.FrameBuffer obtainFrameBuffer(androidx.graphics.opengl.egl.EGLSpec egl);
method public void onDraw(androidx.graphics.opengl.egl.EGLManager eglManager);
- method public void onDrawComplete(androidx.graphics.lowlatency.FrameBuffer frameBuffer, androidx.graphics.lowlatency.SyncFenceCompat? syncFenceCompat);
+ method public void onDrawComplete(androidx.graphics.opengl.FrameBuffer frameBuffer, androidx.hardware.SyncFenceCompat? syncFenceCompat);
}
- public final class FrontBufferSyncStrategy implements androidx.graphics.lowlatency.SyncStrategy {
- ctor public FrontBufferSyncStrategy(long usageFlags);
- method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.graphics.lowlatency.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
- method public boolean isVisible();
- method public void setVisible(boolean);
- property public final boolean isVisible;
- }
-
- @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class GLFrontBufferedRenderer<T> {
- ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback, optional androidx.graphics.opengl.GLRenderer? glRenderer);
- ctor public GLFrontBufferedRenderer(android.view.SurfaceView surfaceView, androidx.graphics.lowlatency.GLFrontBufferedRenderer.Callback<T> callback);
- method public void clear();
- method public void commit();
- method public boolean isValid();
- method public void release(boolean cancelPending, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onReleaseComplete);
- method public void release(boolean cancelPending);
- method public void renderDoubleBufferedLayer(java.util.Collection<? extends T> params);
- method public void renderFrontBufferedLayer(T? param);
- field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
- }
-
- @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
- method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
- method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, java.util.Collection<? extends T> params);
- method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, T? param);
- method @WorkerThread public default void onFrontBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
- }
-
- public static final class GLFrontBufferedRenderer.Companion {
- }
-
- @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFenceCompat implements java.lang.AutoCloseable {
- method public boolean await(long timeoutNanos);
- method public boolean awaitForever();
- method public void close();
- method public static androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec egl);
- method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTimeNanos();
- method public boolean isValid();
- field public static final androidx.graphics.lowlatency.SyncFenceCompat.Companion Companion;
- field public static final long SIGNAL_TIME_INVALID = -1L; // 0xffffffffffffffffL
- field public static final long SIGNAL_TIME_PENDING = 9223372036854775807L; // 0x7fffffffffffffffL
- }
-
- public static final class SyncFenceCompat.Companion {
- method public androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec egl);
- }
-
- public final class SyncFenceCompatKt {
- method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) @kotlin.jvm.JvmSynthetic public static androidx.graphics.lowlatency.SyncFenceCompat createNativeSyncFence(androidx.graphics.opengl.egl.EGLSpec);
- }
-
- public interface SyncStrategy {
- method public androidx.graphics.lowlatency.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
- field public static final androidx.graphics.lowlatency.SyncStrategy ALWAYS;
- field public static final androidx.graphics.lowlatency.SyncStrategy.Companion Companion;
- }
-
- public static final class SyncStrategy.Companion {
- }
-
-}
-
-package androidx.graphics.opengl {
-
public final class GLRenderer {
ctor public GLRenderer(optional kotlin.jvm.functions.Function0<? extends androidx.graphics.opengl.egl.EGLSpec> eglSpecFactory, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EGLManager,? extends android.opengl.EGLConfig> eglConfigFactory);
method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.Surface surface, int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
@@ -95,6 +77,7 @@
method public androidx.graphics.opengl.GLRenderer.RenderTarget createRenderTarget(int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending);
+ method public void execute(Runnable runnable);
method public boolean isRunning();
method public void registerEGLContextCallback(androidx.graphics.opengl.GLRenderer.EGLContextCallback callback);
method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
@@ -132,6 +115,15 @@
method public void resize(int width, int height);
}
+ public interface SyncStrategy {
+ method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFenceCompat? createSyncFence(androidx.graphics.opengl.egl.EGLSpec eglSpec);
+ field public static final androidx.graphics.opengl.SyncStrategy ALWAYS;
+ field public static final androidx.graphics.opengl.SyncStrategy.Companion Companion;
+ }
+
+ public static final class SyncStrategy.Companion {
+ }
+
}
package androidx.graphics.opengl.egl {
@@ -212,7 +204,6 @@
method public boolean eglDestroyImageKHR(androidx.opengl.EGLImageKHR image);
method public boolean eglDestroySurface(android.opengl.EGLSurface surface);
method public boolean eglDestroySyncKHR(androidx.opengl.EGLSyncKHR sync);
- method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(androidx.opengl.EGLSyncKHR sync);
method public android.opengl.EGLSurface eglGetCurrentDrawSurface();
method public android.opengl.EGLSurface eglGetCurrentReadSurface();
method public int eglGetError();
@@ -287,8 +278,8 @@
method public androidx.graphics.surface.SurfaceControlCompat.Transaction reparent(androidx.graphics.surface.SurfaceControlCompat surfaceControl, androidx.graphics.surface.SurfaceControlCompat? newParent);
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public androidx.graphics.surface.SurfaceControlCompat.Transaction reparent(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.view.AttachedSurfaceControl attachedSurfaceControl);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setAlpha(androidx.graphics.surface.SurfaceControlCompat surfaceControl, float alpha);
- method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.graphics.lowlatency.SyncFenceCompat? fence, optional kotlin.jvm.functions.Function0<kotlin.Unit>? releaseCallback);
- method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.graphics.lowlatency.SyncFenceCompat? fence);
+ method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.hardware.SyncFenceCompat? fence, optional kotlin.jvm.functions.Function0<kotlin.Unit>? releaseCallback);
+ method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer, optional androidx.hardware.SyncFenceCompat? fence);
method public androidx.graphics.surface.SurfaceControlCompat.Transaction setBuffer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.hardware.HardwareBuffer buffer);
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);
@@ -308,20 +299,20 @@
package androidx.hardware {
- @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFence implements java.lang.AutoCloseable {
- ctor public SyncFence(int fd);
+ @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public final class SyncFenceCompat implements java.lang.AutoCloseable {
method public boolean await(long timeoutNanos);
method public boolean awaitForever();
method public void close();
- method protected void finalize();
- method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTime();
+ method public static androidx.hardware.SyncFenceCompat createNativeSyncFence();
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public long getSignalTimeNanos();
method public boolean isValid();
- field public static final androidx.hardware.SyncFence.Companion Companion;
+ field public static final androidx.hardware.SyncFenceCompat.Companion Companion;
field public static final long SIGNAL_TIME_INVALID = -1L; // 0xffffffffffffffffL
field public static final long SIGNAL_TIME_PENDING = 9223372036854775807L; // 0x7fffffffffffffffL
}
- public static final class SyncFence.Companion {
+ public static final class SyncFenceCompat.Companion {
+ method public androidx.hardware.SyncFenceCompat createNativeSyncFence();
}
}
@@ -334,7 +325,6 @@
method public static androidx.opengl.EGLSyncKHR? eglCreateSyncKHR(android.opengl.EGLDisplay eglDisplay, int type, androidx.graphics.opengl.egl.EGLConfigAttributes? attributes);
method public static boolean eglDestroyImageKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLImageKHR image);
method public static boolean eglDestroySyncKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR eglSync);
- method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public static androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(android.opengl.EGLDisplay display, androidx.opengl.EGLSyncKHR sync);
method public static boolean eglGetSyncAttribKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR sync, int attribute, int[] value, int offset);
method public static void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
method public static java.util.Set<java.lang.String> parseExtensions(String queryString);
@@ -379,7 +369,6 @@
method public androidx.opengl.EGLSyncKHR? eglCreateSyncKHR(android.opengl.EGLDisplay eglDisplay, int type, androidx.graphics.opengl.egl.EGLConfigAttributes? attributes);
method public boolean eglDestroyImageKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLImageKHR image);
method public boolean eglDestroySyncKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR eglSync);
- method @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) public androidx.hardware.SyncFence eglDupNativeFenceFDANDROID(android.opengl.EGLDisplay display, androidx.opengl.EGLSyncKHR sync);
method public boolean eglGetSyncAttribKHR(android.opengl.EGLDisplay eglDisplay, androidx.opengl.EGLSyncKHR sync, int attribute, int[] value, int offset);
method public void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
method public java.util.Set<java.lang.String> parseExtensions(String queryString);
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
index 61dd09f..9ed71b2 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
@@ -66,19 +66,18 @@
override fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
param: Any
) {
- GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
Matrix.orthoM(
mOrthoMatrix,
0,
0f,
- bufferWidth.toFloat(),
+ bufferInfo.width.toFloat(),
0f,
- bufferHeight.toFloat(),
+ bufferInfo.height.toFloat(),
-1f,
1f
)
@@ -88,19 +87,18 @@
override fun onDrawDoubleBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Any>
) {
- GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
Matrix.orthoM(
mOrthoMatrix,
0,
0f,
- bufferWidth.toFloat(),
+ bufferInfo.width.toFloat(),
0f,
- bufferHeight.toFloat(),
+ bufferInfo.height.toFloat(),
-1f,
1f
)
@@ -170,19 +168,18 @@
override fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
param: Any
) {
- GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
Matrix.orthoM(
mOrthoMatrix,
0,
0f,
- bufferWidth.toFloat(),
+ bufferInfo.width.toFloat(),
0f,
- bufferHeight.toFloat(),
+ bufferInfo.height.toFloat(),
-1f,
1f
)
@@ -192,19 +189,18 @@
override fun onDrawDoubleBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Any>
) {
- GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
Matrix.orthoM(
mOrthoMatrix,
0,
0f,
- bufferWidth.toFloat(),
+ bufferInfo.width.toFloat(),
0f,
- bufferHeight.toFloat(),
+ bufferInfo.height.toFloat(),
-1f,
1f
)
@@ -297,8 +293,7 @@
override fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
param: Int
) {
@@ -307,19 +302,18 @@
override fun onDrawDoubleBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Int>
) {
- GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
Matrix.orthoM(
mOrthoMatrix,
0,
0f,
- bufferWidth.toFloat(),
+ bufferInfo.width.toFloat(),
0f,
- bufferHeight.toFloat(),
+ bufferInfo.height.toFloat(),
-1f,
1f
)
@@ -402,6 +396,356 @@
}
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @Test
+ fun testBufferRetargetingFrontBufferLayer() {
+ val squareSize = 100f
+ val renderLatch = CountDownLatch(1)
+ val callbacks = object : GLFrontBufferedRenderer.Callback<Int> {
+
+ private val mOrthoMatrix = FloatArray(16)
+ private val mProjectionMatrix = FloatArray(16)
+
+ override fun onDrawFrontBufferedLayer(
+ eglManager: EGLManager,
+ bufferInfo: BufferInfo,
+ transform: FloatArray,
+ param: Int
+ ) {
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
+ Matrix.orthoM(
+ mOrthoMatrix,
+ 0,
+ 0f,
+ bufferInfo.width.toFloat(),
+ 0f,
+ bufferInfo.height.toFloat(),
+ -1f,
+ 1f
+ )
+ Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+ val buffer = IntArray(1)
+ GLES20.glGenFramebuffers(1, buffer, 0)
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, buffer[0])
+ Rectangle().draw(transform, Color.RED, 0f, 0f, squareSize, squareSize)
+
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, bufferInfo.frameBufferId)
+ Rectangle().draw(mProjectionMatrix, param, 0f, 0f, squareSize, squareSize)
+ }
+
+ override fun onFrontBufferedLayerRenderComplete(
+ frontBufferedLayerSurfaceControl: SurfaceControlCompat,
+ transaction: SurfaceControlCompat.Transaction
+ ) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ transaction.addTransactionCommittedListener(
+ Executors.newSingleThreadExecutor(),
+ object : SurfaceControlCompat.TransactionCommittedListener {
+ override fun onTransactionCommitted() {
+ renderLatch.countDown()
+ }
+ })
+ } else {
+ renderLatch.countDown()
+ }
+ }
+
+ override fun onDrawDoubleBufferedLayer(
+ eglManager: EGLManager,
+ bufferInfo: BufferInfo,
+ transform: FloatArray,
+ params: Collection<Int>
+ ) {
+ // NO-OP
+ }
+ }
+ var renderer: GLFrontBufferedRenderer<Int>? = null
+ var surfaceView: SurfaceView? = null
+ try {
+ val scenario = ActivityScenario.launch(FrontBufferedRendererTestActivity::class.java)
+ .moveToState(Lifecycle.State.CREATED)
+ .onActivity {
+ surfaceView = it.getSurfaceView()
+ renderer = GLFrontBufferedRenderer(surfaceView!!, callbacks)
+ }
+
+ scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+ renderer?.renderFrontBufferedLayer(Color.BLUE)
+ }
+ assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
+
+ val coords = IntArray(2)
+ with(surfaceView!!) {
+ getLocationOnScreen(coords)
+ }
+
+ SurfaceControlUtils.validateOutput { bitmap ->
+ val center = bitmap.getPixel(
+ coords[0] + (squareSize / 2).toInt(),
+ coords[1] + (squareSize / 2).toInt()
+ )
+ Color.BLUE == center
+ }
+ } finally {
+ renderer.blockingRelease()
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @Test
+ fun testBufferRetargetingDoubleBufferedLayer() {
+ val squareSize = 100f
+ val renderLatch = CountDownLatch(1)
+ val callbacks = object : GLFrontBufferedRenderer.Callback<Int> {
+
+ private val mOrthoMatrix = FloatArray(16)
+ private val mProjectionMatrix = FloatArray(16)
+
+ override fun onDrawFrontBufferedLayer(
+ eglManager: EGLManager,
+ bufferInfo: BufferInfo,
+ transform: FloatArray,
+ param: Int
+ ) {
+ // NO-OP
+ }
+
+ override fun onDrawDoubleBufferedLayer(
+ eglManager: EGLManager,
+ bufferInfo: BufferInfo,
+ transform: FloatArray,
+ params: Collection<Int>
+ ) {
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
+ Matrix.orthoM(
+ mOrthoMatrix,
+ 0,
+ 0f,
+ bufferInfo.width.toFloat(),
+ 0f,
+ bufferInfo.height.toFloat(),
+ -1f,
+ 1f
+ )
+ Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+ val buffer = IntArray(1)
+ GLES20.glGenFramebuffers(1, buffer, 0)
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, buffer[0])
+ Rectangle().draw(transform, Color.RED, 0f, 0f, squareSize, squareSize)
+
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, bufferInfo.frameBufferId)
+ for (param in params) {
+ Rectangle().draw(mProjectionMatrix, param, 0f, 0f, squareSize, squareSize)
+ }
+ }
+
+ override fun onDoubleBufferedLayerRenderComplete(
+ frontBufferedLayerSurfaceControl: SurfaceControlCompat,
+ transaction: SurfaceControlCompat.Transaction
+ ) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ transaction.addTransactionCommittedListener(
+ Executors.newSingleThreadExecutor(),
+ object : SurfaceControlCompat.TransactionCommittedListener {
+ override fun onTransactionCommitted() {
+ renderLatch.countDown()
+ }
+ })
+ } else {
+ renderLatch.countDown()
+ }
+ }
+ }
+
+ var renderer: GLFrontBufferedRenderer<Int>? = null
+ var surfaceView: SurfaceView? = null
+ try {
+ val scenario = ActivityScenario.launch(FrontBufferedRendererTestActivity::class.java)
+ .moveToState(Lifecycle.State.CREATED)
+ .onActivity {
+ surfaceView = it.getSurfaceView()
+ renderer = GLFrontBufferedRenderer(surfaceView!!, callbacks)
+ }
+
+ scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+ renderer?.renderFrontBufferedLayer(Color.BLUE)
+ renderer?.commit()
+ }
+ assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
+
+ val coords = IntArray(2)
+ with(surfaceView!!) {
+ getLocationOnScreen(coords)
+ }
+
+ SurfaceControlUtils.validateOutput { bitmap ->
+ val center = bitmap.getPixel(
+ coords[0] + (squareSize / 2).toInt(),
+ coords[1] + (squareSize / 2).toInt()
+ )
+ Color.BLUE == center
+ }
+ } finally {
+ renderer.blockingRelease()
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @Test
+ fun testCancelFrontBufferLayerRender() {
+ val squareSize = 100f
+ val renderLatch = CountDownLatch(1)
+ val callbacks = object : GLFrontBufferedRenderer.Callback<Int> {
+
+ private val mOrthoMatrix = FloatArray(16)
+ private val mProjectionMatrix = FloatArray(16)
+
+ override fun onDrawFrontBufferedLayer(
+ eglManager: EGLManager,
+ bufferInfo: BufferInfo,
+ transform: FloatArray,
+ param: Int
+ ) {
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
+ Matrix.orthoM(
+ mOrthoMatrix,
+ 0,
+ 0f,
+ bufferInfo.width.toFloat(),
+ 0f,
+ bufferInfo.height.toFloat(),
+ -1f,
+ 1f
+ )
+ Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+ Rectangle().draw(mProjectionMatrix, param, 0f, 0f, squareSize, squareSize)
+ }
+
+ override fun onDrawDoubleBufferedLayer(
+ eglManager: EGLManager,
+ bufferInfo: BufferInfo,
+ transform: FloatArray,
+ params: Collection<Int>
+ ) {
+
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
+ Matrix.orthoM(
+ mOrthoMatrix,
+ 0,
+ 0f,
+ bufferInfo.width.toFloat(),
+ 0f,
+ bufferInfo.height.toFloat(),
+ -1f,
+ 1f
+ )
+ Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+ for (p in params) {
+ Rectangle().draw(mProjectionMatrix, p, 0f, 0f, squareSize, squareSize)
+ }
+ }
+
+ override fun onDoubleBufferedLayerRenderComplete(
+ frontBufferedLayerSurfaceControl: SurfaceControlCompat,
+ transaction: SurfaceControlCompat.Transaction
+ ) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ transaction.addTransactionCommittedListener(
+ Executors.newSingleThreadExecutor(),
+ object : SurfaceControlCompat.TransactionCommittedListener {
+ override fun onTransactionCommitted() {
+ renderLatch.countDown()
+ }
+ })
+ } else {
+ renderLatch.countDown()
+ }
+ }
+ }
+ var renderer: GLFrontBufferedRenderer<Int>? = null
+ var surfaceView: SurfaceView? = null
+ try {
+ val scenario = ActivityScenario.launch(FrontBufferedRendererTestActivity::class.java)
+ .moveToState(Lifecycle.State.CREATED)
+ .onActivity {
+ surfaceView = it.getSurfaceView()
+ renderer = GLFrontBufferedRenderer(surfaceView!!, callbacks)
+ }
+
+ scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+ with(renderer!!) {
+ renderFrontBufferedLayer(Color.BLUE)
+ commit()
+ renderFrontBufferedLayer(Color.RED)
+ cancel()
+ }
+ }
+ assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
+
+ val coords = IntArray(2)
+ with(surfaceView!!) {
+ getLocationOnScreen(coords)
+ }
+
+ SurfaceControlUtils.validateOutput { bitmap ->
+ val pixel = bitmap.getPixel(
+ coords[0] + (squareSize / 2).toInt(),
+ coords[1] + (squareSize / 2).toInt()
+ )
+ // After cancel is invoked the front buffered layer should not be visible
+ Color.BLUE == pixel
+ }
+ } finally {
+ renderer.blockingRelease()
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @Test
+ fun testExecute() {
+ val executeLatch = CountDownLatch(1)
+ val callbacks = object : GLFrontBufferedRenderer.Callback<Int> {
+
+ override fun onDrawFrontBufferedLayer(
+ eglManager: EGLManager,
+ bufferInfo: BufferInfo,
+ transform: FloatArray,
+ param: Int
+ ) {
+ // NO-OP
+ }
+
+ override fun onDrawDoubleBufferedLayer(
+ eglManager: EGLManager,
+ bufferInfo: BufferInfo,
+ transform: FloatArray,
+ params: Collection<Int>
+ ) {
+ // NO-OP
+ }
+ }
+ var renderer: GLFrontBufferedRenderer<Int>? = null
+ var surfaceView: SurfaceView?
+ try {
+ val scenario = ActivityScenario.launch(FrontBufferedRendererTestActivity::class.java)
+ .moveToState(Lifecycle.State.CREATED)
+ .onActivity {
+ surfaceView = it.getSurfaceView()
+ renderer = GLFrontBufferedRenderer(surfaceView!!, callbacks)
+ }
+
+ scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+ renderer?.execute {
+ executeLatch.countDown()
+ }
+ }
+
+ assertTrue(executeLatch.await(3000, TimeUnit.MILLISECONDS))
+ } finally {
+ renderer.blockingRelease()
+ }
+ }
+
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
fun testUsageFlagContainsFrontBufferUsage() {
@@ -456,19 +800,18 @@
override fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
param: Any
) {
- GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
Matrix.orthoM(
mOrthoMatrix,
0,
0f,
- bufferWidth.toFloat(),
+ bufferInfo.width.toFloat(),
0f,
- bufferHeight.toFloat(),
+ bufferInfo.height.toFloat(),
-1f,
1f
)
@@ -483,19 +826,18 @@
override fun onDrawDoubleBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Any>
) {
- GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
Matrix.orthoM(
mOrthoMatrix,
0,
0f,
- bufferWidth.toFloat(),
+ bufferInfo.width.toFloat(),
0f,
- bufferHeight.toFloat(),
+ bufferInfo.height.toFloat(),
-1f,
1f
)
@@ -536,19 +878,18 @@
val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
override fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
param: Any
) {
- GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
Matrix.orthoM(
mOrthoMatrix,
0,
0f,
- bufferWidth.toFloat(),
+ bufferInfo.width.toFloat(),
0f,
- bufferHeight.toFloat(),
+ bufferInfo.height.toFloat(),
-1f,
1f
)
@@ -564,19 +905,18 @@
override fun onDrawDoubleBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Any>
) {
- GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
Matrix.orthoM(
mOrthoMatrix,
0,
0f,
- bufferWidth.toFloat(),
+ bufferInfo.width.toFloat(),
0f,
- bufferHeight.toFloat(),
+ bufferInfo.height.toFloat(),
-1f,
1f
)
@@ -663,19 +1003,18 @@
override fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
param: Any
) {
- GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
Matrix.orthoM(
mOrthoMatrix,
0,
0f,
- bufferWidth.toFloat(),
+ bufferInfo.width.toFloat(),
0f,
- bufferHeight.toFloat(),
+ bufferInfo.height.toFloat(),
-1f,
1f
)
@@ -685,19 +1024,18 @@
override fun onDrawDoubleBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Any>
) {
- GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
Matrix.orthoM(
mOrthoMatrix,
0,
0f,
- bufferWidth.toFloat(),
+ bufferInfo.width.toFloat(),
0f,
- bufferHeight.toFloat(),
+ bufferInfo.height.toFloat(),
-1f,
1f
)
@@ -794,19 +1132,18 @@
override fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
param: Any
) {
- GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
Matrix.orthoM(
mOrthoMatrix,
0,
0f,
- bufferWidth.toFloat(),
+ bufferInfo.width.toFloat(),
0f,
- bufferHeight.toFloat(),
+ bufferInfo.height.toFloat(),
-1f,
1f
)
@@ -816,19 +1153,18 @@
override fun onDrawDoubleBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Any>
) {
- GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
Matrix.orthoM(
mOrthoMatrix,
0,
0f,
- bufferWidth.toFloat(),
+ bufferInfo.width.toFloat(),
0f,
- bufferHeight.toFloat(),
+ bufferInfo.height.toFloat(),
-1f,
1f
)
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt
index 0e5642c..160db4b 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt
@@ -63,19 +63,18 @@
override fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
param: FloatArray
) {
- GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
Matrix.orthoM(
mMVPMatrix,
0,
0f,
- bufferWidth.toFloat(),
+ bufferInfo.width.toFloat(),
0f,
- bufferHeight.toFloat(),
+ bufferInfo.height.toFloat(),
-1f,
1f
)
@@ -94,21 +93,20 @@
override fun onDrawDoubleBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<FloatArray>
) {
- GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ GLES20.glViewport(0, 0, bufferInfo.width, bufferInfo.height)
GLES20.glClearColor(0f, 0f, 0f, 0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
Matrix.orthoM(
mMVPMatrix,
0,
0f,
- bufferWidth.toFloat(),
+ bufferInfo.width.toFloat(),
0f,
- bufferHeight.toFloat(),
+ bufferInfo.height.toFloat(),
-1f,
1f
)
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/FrameBufferPoolTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/FrameBufferPoolTest.kt
similarity index 98%
rename from graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/FrameBufferPoolTest.kt
rename to graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/FrameBufferPoolTest.kt
index b6b2055..95d473d 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/FrameBufferPoolTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/FrameBufferPoolTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.graphics.lowlatency
+package androidx.graphics.opengl
import android.hardware.HardwareBuffer
import android.os.Build
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
index 313ac2d..7fb8eb5 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
@@ -37,11 +37,9 @@
import android.view.TextureView
import androidx.annotation.RequiresApi
import androidx.annotation.WorkerThread
-import androidx.graphics.lowlatency.FrameBufferRenderer
-import androidx.graphics.lowlatency.FrameBuffer
import androidx.graphics.lowlatency.LineRenderer
import androidx.graphics.lowlatency.Rectangle
-import androidx.graphics.lowlatency.SyncFenceCompat
+import androidx.hardware.SyncFenceCompat
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
import androidx.graphics.opengl.egl.supportsNativeAndroidFence
@@ -367,6 +365,18 @@
}
@Test
+ fun testExecute() {
+ val countDownLatch = CountDownLatch(1)
+ GLRenderer().apply {
+ start()
+ execute {
+ countDownLatch.countDown()
+ }
+ }
+ assertTrue(countDownLatch.await(3000, TimeUnit.MILLISECONDS))
+ }
+
+ @Test
fun testNonStartedGLRendererIsNotRunning() {
assertFalse(GLRenderer().isRunning())
}
@@ -759,7 +769,7 @@
GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
GLES20.glFlush()
- syncFenceCompat = SyncFenceCompat.createNativeSyncFence(egl)
+ syncFenceCompat = SyncFenceCompat.createNativeSyncFence()
syncFenceCompat.await(TimeUnit.SECONDS.toNanos(3))
} finally {
syncFenceCompat?.close()
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SyncStrategyTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncStrategyTest.kt
similarity index 96%
rename from graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SyncStrategyTest.kt
rename to graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncStrategyTest.kt
index b8a00d2..f47e622 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SyncStrategyTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncStrategyTest.kt
@@ -14,11 +14,13 @@
* limitations under the License.
*/
-package androidx.graphics.lowlatency
+package androidx.graphics.opengl
import android.opengl.EGL14
import android.os.Build
import androidx.annotation.RequiresApi
+import androidx.graphics.lowlatency.FrontBufferSyncStrategy
+import androidx.graphics.lowlatency.GLFrontBufferedRenderer
import androidx.graphics.opengl.egl.EGLConfigAttributes
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
index 6e92aef..d2a9139 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
@@ -28,6 +28,7 @@
import android.os.Build
import android.view.Surface
import androidx.annotation.RequiresApi
+import androidx.hardware.SyncFenceCompat
import androidx.opengl.EGLBindings
import androidx.opengl.EGLExt
import androidx.opengl.EGLExt.Companion.EGL_ANDROID_CLIENT_BUFFER
@@ -701,7 +702,8 @@
GLES20.glFlush()
assertEquals("glFlush failed", GLES20.GL_NO_ERROR, GLES20.glGetError())
- val syncFence = eglSpec.eglDupNativeFenceFDANDROID(sync!!)
+ val display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
+ val syncFence = EGLExt.eglDupNativeFenceFDANDROID(display, sync!!)
assertTrue(syncFence.isValid())
assertTrue(syncFence.await(TimeUnit.MILLISECONDS.toNanos(3000)))
@@ -726,16 +728,17 @@
GLES20.glFlush()
assertEquals("glFlush failed", GLES20.GL_NO_ERROR, GLES20.glGetError())
- val syncFence = eglSpec.eglDupNativeFenceFDANDROID(sync!!)
+ val display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
+ val syncFence = EGLExt.eglDupNativeFenceFDANDROID(display, sync!!)
assertTrue(syncFence.isValid())
- assertNotEquals(SyncFence.SIGNAL_TIME_INVALID, syncFence.getSignalTime())
+ assertNotEquals(SyncFenceCompat.SIGNAL_TIME_INVALID, syncFence.getSignalTimeNanos())
assertTrue(syncFence.awaitForever())
assertTrue(eglSpec.eglDestroySyncKHR(sync))
assertEquals("eglDestroySyncKHR failed", EGL14.EGL_SUCCESS, EGL14.eglGetError())
syncFence.close()
assertFalse(syncFence.isValid())
- assertEquals(SyncFence.SIGNAL_TIME_INVALID, syncFence.getSignalTime())
+ assertEquals(SyncFence.SIGNAL_TIME_INVALID, syncFence.getSignalTimeNanos())
}
}
}
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 a6e3653..b475f0b 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
@@ -24,7 +24,7 @@
import android.os.Build
import android.os.SystemClock
import android.view.SurfaceHolder
-import androidx.graphics.lowlatency.SyncFenceCompat
+import androidx.hardware.SyncFenceCompat
import androidx.graphics.opengl.egl.EGLConfigAttributes
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
@@ -495,7 +495,7 @@
assertNotNull(buffer)
val fence = if (manager.supportsNativeAndroidFence()) {
- SyncFenceCompat.createNativeSyncFence(manager.eglSpec)
+ SyncFenceCompat.createNativeSyncFence()
} else {
null
}
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncFenceCompatTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceCompatTest.kt
similarity index 94%
rename from graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncFenceCompatTest.kt
rename to graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceCompatTest.kt
index f1610d8..af174be 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncFenceCompatTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceCompatTest.kt
@@ -14,19 +14,17 @@
* limitations under the License.
*/
-package androidx.graphics.opengl
+package androidx.hardware
import android.opengl.EGL14
import android.opengl.GLES20
import android.os.Build
import androidx.annotation.RequiresApi
-import androidx.graphics.lowlatency.SyncFenceCompat
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.hardware.SyncFence
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
@@ -45,7 +43,7 @@
testEglManager {
initializeWithDefaultConfig()
if (supportsNativeAndroidFence()) {
- val syncFenceCompat = SyncFenceCompat.createNativeSyncFence(this.eglSpec)
+ val syncFenceCompat = SyncFenceCompat.createNativeSyncFence()
assert(syncFenceCompat.isValid())
syncFenceCompat.close()
}
@@ -58,7 +56,7 @@
initializeWithDefaultConfig()
if (supportsNativeAndroidFence()) {
- val syncFenceCompat = SyncFenceCompat.createNativeSyncFence(this.eglSpec)
+ val syncFenceCompat = SyncFenceCompat.createNativeSyncFence()
assert(syncFenceCompat.isValid())
GLES20.glFlush()
assertTrue(syncFenceCompat.await(1000))
@@ -73,7 +71,7 @@
testEglManager {
initializeWithDefaultConfig()
if (supportsNativeAndroidFence()) {
- val syncFenceCompat = SyncFenceCompat.createNativeSyncFence(this.eglSpec)
+ val syncFenceCompat = SyncFenceCompat.createNativeSyncFence()
assert(syncFenceCompat.isValid())
assertTrue(syncFenceCompat.awaitForever())
@@ -89,9 +87,10 @@
initializeWithDefaultConfig()
if (supportsNativeAndroidFence()) {
val start = System.nanoTime()
- val syncFenceCompat = SyncFenceCompat.createNativeSyncFence(this.eglSpec)
+ val syncFenceCompat = SyncFenceCompat.createNativeSyncFence()
assertTrue(syncFenceCompat.isValid())
- assertTrue(syncFenceCompat.getSignalTimeNanos() != SyncFence.SIGNAL_TIME_INVALID)
+ assertTrue(syncFenceCompat.getSignalTimeNanos() !=
+ SyncFenceCompat.SIGNAL_TIME_INVALID)
assertTrue(syncFenceCompat.awaitForever())
assertTrue(syncFenceCompat.getSignalTimeNanos() > start)
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceV19Test.kt
similarity index 87%
rename from graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceTest.kt
rename to graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceV19Test.kt
index 3c8f421..7bedd9b 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceV19Test.kt
@@ -31,12 +31,12 @@
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
@SmallTest
-class SyncFenceTest {
+class SyncFenceV19Test {
@Test
fun testDupSyncFenceFd() {
val fileDescriptor = 7
- val syncFence = SyncFence(7)
+ val syncFence = SyncFenceV19(7)
// If the file descriptor is valid dup'ing it should return a different fd
Assert.assertNotEquals(fileDescriptor, JniBindings.nDupFenceFd(syncFence))
}
@@ -44,7 +44,7 @@
@Test
fun testWaitMethodLink() {
try {
- SyncFence(8).await(1000)
+ SyncFenceV19(8).await(1000)
} catch (linkError: UnsatisfiedLinkError) {
fail("Unable to resolve wait method")
} catch (exception: Exception) {
@@ -56,7 +56,7 @@
fun testDupSyncFenceFdWhenInvalid() {
// If the fence is invalid there should be no attempt to dup the fd it and -1
// should be returned
- Assert.assertEquals(-1, JniBindings.nDupFenceFd(SyncFence(-1)))
+ Assert.assertEquals(-1, JniBindings.nDupFenceFd(SyncFenceV19(-1)))
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@@ -67,19 +67,19 @@
// Because not all devices support the ability to create a native file descriptor from
// an EGLSync, create a validity check to ensure we can get more presubmit test coverage
Assert.assertEquals(
- SyncFence.SIGNAL_TIME_INVALID,
- SyncFence(7).getSignalTime()
+ SyncFenceCompat.SIGNAL_TIME_INVALID,
+ SyncFenceV19(7).getSignalTimeNanos()
)
Assert.assertEquals(
- SyncFence.SIGNAL_TIME_INVALID,
- SyncFence(-1).getSignalTime()
+ SyncFenceCompat.SIGNAL_TIME_INVALID,
+ SyncFenceV19(-1).getSignalTimeNanos()
)
}
@Test
fun testIsValid() {
- assertFalse(SyncFence(-1).isValid())
- assertTrue(SyncFence(42).isValid())
+ assertFalse(SyncFenceV19(-1).isValid())
+ assertTrue(SyncFenceV19(42).isValid())
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
diff --git a/graphics/graphics-core/src/main/cpp/graphics-core.cpp b/graphics/graphics-core/src/main/cpp/graphics-core.cpp
index 564ea65..d7c44a7 100644
--- a/graphics/graphics-core/src/main/cpp/graphics-core.cpp
+++ b/graphics/graphics-core/src/main/cpp/graphics-core.cpp
@@ -266,7 +266,7 @@
void setupSyncFenceClassInfo(JNIEnv *env) {
if (!gSyncFenceClassInfo.CLASS_INFO_INITIALIZED) {
- jclass syncFenceClazz = env->FindClass("androidx/hardware/SyncFence");
+ jclass syncFenceClazz = env->FindClass("androidx/hardware/SyncFenceV19");
gSyncFenceClassInfo.clazz = static_cast<jclass>(env->NewGlobalRef(syncFenceClazz));
gSyncFenceClassInfo.dupeFileDescriptor =
env->GetMethodID(gSyncFenceClassInfo.clazz, "dupeFileDescriptor", "()I");
@@ -496,12 +496,12 @@
},
{
"nDupFenceFd",
- "(Landroidx/hardware/SyncFence;)I",
+ "(Landroidx/hardware/SyncFenceV19;)I",
(void *) JniBindings_nDupFenceFd
},
{
"nSetBuffer",
- "(JJLandroid/hardware/HardwareBuffer;Landroidx/hardware/SyncFence;)V",
+ "(JJLandroid/hardware/HardwareBuffer;Landroidx/hardware/SyncFenceV19;)V",
(void *) JniBindings_nSetBuffer
},
{
diff --git a/graphics/graphics-core/src/main/cpp/sync_fence.cpp b/graphics/graphics-core/src/main/cpp/sync_fence.cpp
index b0ddb2a..68d18e3 100644
--- a/graphics/graphics-core/src/main/cpp/sync_fence.cpp
+++ b/graphics/graphics-core/src/main/cpp/sync_fence.cpp
@@ -239,7 +239,7 @@
};
jint loadSyncFenceMethods(JNIEnv* env) {
- jclass syncFenceClass = env->FindClass("androidx/hardware/SyncFence");
+ jclass syncFenceClass = env->FindClass("androidx/hardware/SyncFenceV19");
if (syncFenceClass == nullptr) {
return JNI_ERR;
}
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferInfo.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferInfo.kt
new file mode 100644
index 0000000..862799f
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferInfo.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.opengl.GLES20
+
+/**
+ * Class that represents information about the current buffer that is target for rendered output
+ *
+ * @param width Current width of the buffer taking pre-rotation into account.
+ * @param height Current height of the buffer taking pre-rotation into account
+ * @param frameBufferId Frame buffer object identifier. This is useful for retargeting rendering
+ * operations to the original destination after rendering to intermediate scratch buffers.
+ */
+class BufferInfo internal constructor(
+ width: Int = 0,
+ height: Int = 0,
+ frameBufferId: Int = -1
+) {
+
+ /**
+ * Width of the buffer that is being rendered into. This can be different than the corresponding
+ * dimensions specified as pre-rotation can occasionally swap width and height parameters in
+ * order to avoid GPU composition to rotate content. This should be used as input to
+ * [GLES20.glViewport].
+ */
+ var width: Int = width
+ internal set
+
+ /**
+ * Height of the buffer that is being rendered into. This can be different than the
+ * corresponding dimensions specified as pre-rotation can occasionally swap width and height
+ * parameters in order to avoid GPU composition to rotate content. This should be used as input
+ * to [GLES20.glViewport].
+ */
+ var height: Int = height
+ internal set
+
+ /**
+ * Identifier of the destination frame buffer object that is being rendered into. This is
+ * useful for re-binding to the original target after rendering to intermediate frame buffer
+ * objects.
+ */
+ var frameBufferId: Int = frameBufferId
+ internal set
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrontBufferSyncStrategy.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrontBufferSyncStrategy.kt
new file mode 100644
index 0000000..a7579b7
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrontBufferSyncStrategy.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.graphics.lowlatency
+
+import android.hardware.HardwareBuffer
+import android.opengl.GLES20
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.graphics.opengl.FrameBufferRenderer
+import androidx.graphics.opengl.SyncStrategy
+import androidx.graphics.opengl.egl.EGLSpec
+import androidx.hardware.SyncFenceCompat
+
+/**
+ * [SyncStrategy] implementation that optimizes for front buffered rendering use cases.
+ * More specifically this attempts to avoid unnecessary synchronization overhead
+ * wherever possible.
+ *
+ * This will always provide a fence if the corresponding layer transitions from
+ * an invisible to a visible state. If the layer is already visible and front
+ * buffer usage flags are support on the device, then no fence is provided. If this
+ * flag is not supported, then a fence is created to ensure contents
+ * are flushed to the single buffer.
+ *
+ * @param usageFlags usage flags that describe the [HardwareBuffer] that is used as the destination
+ * for rendering content within [FrameBufferRenderer]. The usage flags can be obtained via
+ * [HardwareBuffer.getUsage] or by passing in the same flags from [HardwareBuffer.create]
+ */
+class FrontBufferSyncStrategy(
+ usageFlags: Long
+) : SyncStrategy {
+ private val supportsFrontBufferUsage = (usageFlags and HardwareBuffer.USAGE_FRONT_BUFFER) != 0L
+ private var mFrontBufferVisible: Boolean = false
+
+ /**
+ * Tells whether the corresponding front buffer layer is visible in its current state or not.
+ * Utilize this to dictate when a [SyncFenceCompat] will be created when using
+ * [createSyncFence].
+ */
+ var isVisible
+ get() = mFrontBufferVisible
+ set(visibility) {
+ mFrontBufferVisible = visibility
+ }
+
+ /**
+ * Creates a [SyncFenceCompat] based on various conditions.
+ * If the layer is changing from invisible to visible, a fence is provided.
+ * If the layer is already visible and front buffer usage flag is supported on the device, then
+ * no fence is provided.
+ * If front buffer usage is not supported, then a fence is created and destroyed to flush
+ * contents to screen.
+ */
+ @RequiresApi(Build.VERSION_CODES.KITKAT)
+ override fun createSyncFence(eglSpec: EGLSpec): SyncFenceCompat? {
+ return if (!isVisible) {
+ SyncFenceCompat.createNativeSyncFence()
+ } else if (supportsFrontBufferUsage) {
+ GLES20.glFlush()
+ return null
+ } else {
+ val fence = SyncFenceCompat.createNativeSyncFence()
+ fence.close()
+ return null
+ }
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
index 25ebb92..829e606 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
@@ -25,10 +25,14 @@
import android.view.SurfaceView
import androidx.annotation.RequiresApi
import androidx.annotation.WorkerThread
+import androidx.graphics.opengl.FrameBuffer
+import androidx.graphics.opengl.FrameBufferPool
+import androidx.graphics.opengl.FrameBufferRenderer
import androidx.graphics.opengl.GLRenderer
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
import androidx.graphics.surface.SurfaceControlCompat
+import androidx.hardware.SyncFenceCompat
import androidx.opengl.EGLExt.Companion.EGL_ANDROID_NATIVE_FENCE_SYNC
import androidx.opengl.EGLExt.Companion.EGL_KHR_FENCE_SYNC
import java.util.concurrent.ConcurrentLinkedQueue
@@ -167,6 +171,19 @@
}
/**
+ * Runnable executed on the GLThread to update [FrontBufferSyncStrategy.isVisible] as well
+ * as hide the SurfaceControl associated with the front buffered layer
+ */
+ private val mCancelRunnable = Runnable {
+ mFrontBufferSyncStrategy.isVisible = false
+ mFrontBufferedLayerSurfaceControl?.let { frontBufferSurfaceControl ->
+ SurfaceControlCompat.Transaction()
+ .setVisibility(frontBufferSurfaceControl, false)
+ .commit()
+ }
+ }
+
+ /**
* Queue of parameters to be consumed in [Callback.onDrawFrontBufferedLayer] with the parameter
* provided in [renderFrontBufferedLayer]
*/
@@ -442,6 +459,39 @@
}
/**
+ * Requests to cancel rendering and hides the front buffered layer.
+ * Unlike [commit], this does not schedule a call to render into the double buffered layer.
+ *
+ * If this [GLFrontBufferedRenderer] has been released, that is [isValid] returns `false`,
+ * this call is ignored.
+ */
+ fun cancel() {
+ if (isValid()) {
+ mActiveSegment.clear()
+ mGLRenderer.execute(mCancelRunnable)
+ mFrontBufferedLayerRenderer?.clear()
+ } else {
+ Log.w(TAG, "Attempt to cancel rendering to front buffer after " +
+ "GLFrontBufferedRenderer has been released")
+ }
+ }
+
+ /**
+ * Queue a [Runnable] to be executed on the GL rendering thread. Note it is important
+ * this [Runnable] does not block otherwise it can stall the GL thread.
+ *
+ * @param runnable to be executed
+ */
+ fun execute(runnable: Runnable) {
+ if (isValid()) {
+ mGLRenderer.execute(runnable)
+ } else {
+ Log.w(TAG, "Attempt to execute runnable after GLFrontBufferedRenderer has " +
+ "been released")
+ }
+ }
+
+ /**
* Helper method used to detach the front and multi buffered render targets as well as
* release SurfaceControl instances
*/
@@ -525,6 +575,7 @@
bufferHeight: Int,
usageFlags: Long
): FrameBufferRenderer {
+ val bufferInfo = BufferInfo()
return FrameBufferRenderer(
object : FrameBufferRenderer.RenderCallback {
private fun createFrontBufferLayer(usageFlags: Long): HardwareBuffer {
@@ -547,6 +598,7 @@
createFrontBufferLayer(usageFlags)
).also {
mFrontLayerBuffer = it
+ bufferInfo.frameBufferId = it.frameBuffer
}
}
return buffer
@@ -554,11 +606,14 @@
@WorkerThread
override fun onDraw(eglManager: EGLManager) {
+ bufferInfo.apply {
+ this.width = mParentRenderLayer.getBufferWidth()
+ this.height = mParentRenderLayer.getBufferHeight()
+ }
mActiveSegment.next { param ->
mCallback.onDrawFrontBufferedLayer(
eglManager,
- mParentRenderLayer.getBufferWidth(),
- mParentRenderLayer.getBufferHeight(),
+ bufferInfo,
mParentRenderLayer.getTransform(),
param
)
@@ -661,16 +716,14 @@
* parameters.
* @param eglManager [EGLManager] useful in configuring EGL objects to be used when issuing
* OpenGL commands to render into the front buffered layer
- * @param bufferWidth Width of the buffer that is being rendered into. This can be different
- * than the corresponding dimensions of the [SurfaceView] provided to the
- * [GLFrontBufferedRenderer] as pre-rotation can occasionally swap width and height
- * parameters in order to avoid GPU composition to rotate content. This should be used
- * as input to [GLES20.glViewport].
- * @param bufferHeight Height of the buffer that is being rendered into. This can be different
- * than the corresponding dimensions of the [SurfaceView] provided to the
- * [GLFrontBufferedRenderer] as pre-rotation can occasionally swap width and height
- * parameters in order to avoid GPU composition to rotate content. This should be used as
- * input to [GLES20.glViewport].
+ * @param bufferInfo [BufferInfo] about the buffer that is being rendered into. This
+ * includes the width and height of the buffer which can be different than the corresponding
+ * dimensions of the [SurfaceView] provided to the [GLFrontBufferedRenderer] as pre-rotation
+ * can occasionally swap width and height parameters in order to avoid GPU composition to
+ * rotate content. This should be used as input to [GLES20.glViewport].
+ * Additionally this also contains a frame buffer identifier that can be used to retarget
+ * rendering operations to the original destination after rendering into intermediate
+ * scratch buffers.
* @param transform Matrix that should be applied to the rendering in this callback.
* This should be consumed as input to any vertex shader implementations. Buffers are
* pre-rotated in advance in order to avoid unnecessary overhead of GPU composition to
@@ -685,9 +738,9 @@
* myMatrix, // matrix
* 0, // offset starting index into myMatrix
* 0f, // left
- * bufferWidth.toFloat(), // right
+ * bufferInfo.bufferWidth.toFloat(), // right
* 0f, // bottom
- * bufferHeight.toFloat(), // top
+ * bufferInfo.bufferHeight.toFloat(), // top
* -1f, // near
* 1f, // far
* )
@@ -701,8 +754,7 @@
@WorkerThread
fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
param: T
)
@@ -712,16 +764,14 @@
* parameters.
* @param eglManager [EGLManager] useful in configuring EGL objects to be used when issuing
* OpenGL commands to render into the double buffered layer
- * @param bufferWidth Width of the buffer that is being rendered into. This can be different
- * than the corresponding dimensions of the [SurfaceView] provided to the
- * [GLFrontBufferedRenderer] as pre-rotation can occasionally swap width and height
- * parameters in order to avoid GPU composition to rotate content. This should be used
- * as input to [GLES20.glViewport].
- * @param bufferHeight Height of the buffer that is being rendered into. This can be different
- * than the corresponding dimensions of the [SurfaceView] provided to the
- * [GLFrontBufferedRenderer] as pre-rotation can occasionally swap width and height
- * parameters in order to avoid GPU composition to rotate content. This should be used as
- * input to [GLES20.glViewport].
+ * @param bufferInfo [BufferInfo] about the buffer that is being rendered into. This
+ * includes the width and height of the buffer which can be different than the corresponding
+ * dimensions of the [SurfaceView] provided to the [GLFrontBufferedRenderer] as pre-rotation
+ * can occasionally swap width and height parameters in order to avoid GPU composition to
+ * rotate content. This should be used as input to [GLES20.glViewport].
+ * Additionally this also contains a frame buffer identifier that can be used to retarget
+ * rendering operations to the original destination after rendering into intermediate
+ * scratch buffers.
* @param transform Matrix that should be applied to the rendering in this callback.
* This should be consumed as input to any vertex shader implementations. Buffers are
* pre-rotated in advance in order to avoid unnecessary overhead of GPU composition to
@@ -736,9 +786,9 @@
* myMatrix, // matrix
* 0, // offset starting index into myMatrix
* 0f, // left
- * bufferWidth.toFloat(), // right
+ * bufferInfo.bufferWidth.toFloat(), // right
* 0f, // bottom
- * bufferHeight.toFloat(), // top
+ * bufferInfo.bufferHeight.toFloat(), // top
* -1f, // near
* 1f, // far
* )
@@ -777,8 +827,7 @@
@WorkerThread
fun onDrawDoubleBufferedLayer(
eglManager: EGLManager,
- bufferWidth: Int,
- bufferHeight: Int,
+ bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<T>
)
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
index fbdaf57..b702a35 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
@@ -16,6 +16,7 @@
package androidx.graphics.lowlatency
+import androidx.graphics.opengl.FrameBufferPool
import androidx.graphics.opengl.GLRenderer
import androidx.graphics.surface.SurfaceControlCompat
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
index 5355039..f82da69 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
@@ -22,10 +22,13 @@
import android.view.SurfaceHolder
import android.view.SurfaceView
import androidx.annotation.RequiresApi
+import androidx.graphics.opengl.FrameBuffer
+import androidx.graphics.opengl.FrameBufferRenderer
import androidx.graphics.opengl.GLRenderer
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
import androidx.graphics.surface.SurfaceControlCompat
+import androidx.hardware.SyncFenceCompat
import java.util.Collections
/**
@@ -101,18 +104,25 @@
renderLayerCallback: GLFrontBufferedRenderer.Callback<T>
): GLRenderer.RenderTarget {
var params: Collection<T>? = null
+ val bufferInfo = BufferInfo()
val frameBufferRenderer = FrameBufferRenderer(
object : FrameBufferRenderer.RenderCallback {
- override fun obtainFrameBuffer(egl: EGLSpec): FrameBuffer =
- mLayerCallback?.getFrameBufferPool()?.obtain(egl)
+ override fun obtainFrameBuffer(egl: EGLSpec): FrameBuffer {
+ val frameBuffer = mLayerCallback?.getFrameBufferPool()?.obtain(egl)
?: throw IllegalArgumentException("No FrameBufferPool available")
+ bufferInfo.frameBufferId = frameBuffer.frameBuffer
+ return frameBuffer
+ }
override fun onDraw(eglManager: EGLManager) {
+ bufferInfo.apply {
+ this.width = mBufferTransform.glWidth
+ this.height = mBufferTransform.glHeight
+ }
renderLayerCallback.onDrawDoubleBufferedLayer(
eglManager,
- mBufferTransform.glWidth,
- mBufferTransform.glHeight,
+ bufferInfo,
mBufferTransform.transform,
params ?: Collections.emptyList()
)
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceV19.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceV19.kt
deleted file mode 100644
index 869ad67..0000000
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceV19.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.graphics.lowlatency
-
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.hardware.SyncFence
-
-@RequiresApi(Build.VERSION_CODES.KITKAT)
-internal class SyncFenceV19(syncFence: SyncFence) : SyncFenceImpl {
- internal val mSyncFence: SyncFence = syncFence
-
- /**
- * See [SyncFenceImpl.await]
- */
- override fun await(timeoutNanos: Long): Boolean {
- return mSyncFence.await(timeoutNanos)
- }
-
- /**
- * See [SyncFenceImpl.awaitForever]
- */
- override fun awaitForever(): Boolean {
- return mSyncFence.awaitForever()
- }
-
- /**
- * See [SyncFenceImpl.close]
- */
- override fun close() {
- mSyncFence.close()
- }
-
- /**
- * See [SyncFenceImpl.getSignalTimeNanos]
- */
- @RequiresApi(Build.VERSION_CODES.O)
- override fun getSignalTimeNanos(): Long {
- return mSyncFence.getSignalTime()
- }
-
- /**
- * See [SyncFenceImpl.isValid]
- */
- override fun isValid(): Boolean {
- return mSyncFence.isValid()
- }
-}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/WrapperFrameBufferRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/WrapperFrameBufferRenderer.kt
index 005b06d..a80712b 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/WrapperFrameBufferRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/WrapperFrameBufferRenderer.kt
@@ -21,6 +21,7 @@
import android.os.Build
import android.view.Surface
import androidx.annotation.RequiresApi
+import androidx.graphics.opengl.FrameBufferRenderer
import androidx.graphics.opengl.GLRenderer
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBuffer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBuffer.kt
similarity index 92%
rename from graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBuffer.kt
rename to graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBuffer.kt
index 4f238d0..cee697f 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBuffer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBuffer.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.graphics.lowlatency
+package androidx.graphics.opengl
import android.hardware.HardwareBuffer
import android.opengl.GLES20
@@ -31,7 +31,7 @@
* that is loaded as a texture.
*
* @param egl [EGLSpec] used to specify EGL version and call various EGL methods
- * @property hardwareBuffer the [HardwareBuffer] that this class wraps and used to generate a
+ * @param hardwareBuffer the [HardwareBuffer] that this class wraps and used to generate a
* [EGLImageKHR] object
*/
@RequiresApi(Build.VERSION_CODES.O)
@@ -42,7 +42,12 @@
private var eglImage: EGLImageKHR?
private var texture: Int = -1
- private var frameBuffer: Int = -1
+
+ /**
+ * Return the corresponding FrameBuffer identifier.
+ */
+ internal var frameBuffer: Int = -1
+ private set
/**
* Boolean that tells if the frame buffer is currently closed
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferPool.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBufferPool.kt
similarity index 98%
rename from graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferPool.kt
rename to graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBufferPool.kt
index 29acb84e..17da756 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferPool.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBufferPool.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.graphics.lowlatency
+package androidx.graphics.opengl
import android.hardware.HardwareBuffer
import android.os.Build
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBufferRenderer.kt
similarity index 73%
rename from graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferRenderer.kt
rename to graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBufferRenderer.kt
index bf68dd4..0ec4582 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/FrameBufferRenderer.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.graphics.lowlatency
+package androidx.graphics.opengl
import android.annotation.SuppressLint
import android.hardware.HardwareBuffer
@@ -25,7 +25,7 @@
import android.util.Log
import android.view.Surface
import androidx.annotation.RequiresApi
-import androidx.graphics.opengl.GLRenderer
+import androidx.hardware.SyncFenceCompat
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
import androidx.opengl.EGLExt
@@ -36,6 +36,47 @@
/**
* [GLRenderer.RenderCallback] implementation that renders content into a frame buffer object
* backed by a [HardwareBuffer] object
+ *
+ * @param frameBufferRendererCallbacks Callbacks to provide a [FrameBuffer] instance to render into,
+ * draw method to render into the [FrameBuffer] as well as a subsequent callback to consume the
+ * contents of the [FrameBuffer]
+ * @param syncStrategy [SyncStrategy] used to determine when a fence is to be created to gate on
+ * consumption of the [FrameBuffer] instance. This determines if a [SyncFenceCompat] instance is
+ * provided in the [RenderCallback.onDrawComplete] depending on the use case.
+ * For example for front buffered rendering scenarios, it is possible that no [SyncFenceCompat] is
+ * provided in order to reduce latency within the rendering pipeline.
+ *
+ * This API can be used to render content into a [HardwareBuffer] directly and convert that to a
+ * bitmap with the following code snippet:
+ *
+ * val glRenderer = GLRenderer().apply { start() }
+ * val callbacks = object : FrameBufferRenderer.RenderCallback {
+ *
+ * override fun obtainFrameBuffer(egl: EGLSpec): FrameBuffer =
+ * FrameBuffer(
+ * egl,
+ * HardwareBuffer.create(
+ * width,
+ * height,
+ * HardwareBuffer.RGBA_8888,
+ * 1,
+ * HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
+ * )
+ * )
+ *
+ * override fun onDraw(eglManager: EGLManager) {
+ * // GL code
+ * }
+ *
+ * override fun onDrawComplete(frameBuffer: FrameBuffer, syncFenceCompat: SyncFenceCompat?) {
+ * syncFenceCompat?.awaitForever()
+ * val bitmap = Bitmap.wrapHardwareBuffer(frameBuffer.hardwareBuffer,
+ * ColorSpace.get(ColorSpace.Named.LINEAR_SRGB))
+ * // bitmap operations
+ * }
+ * }
+ *
+ * glRenderer.createRenderTarget(width,height, FrameBufferRenderer(callbacks)).requestRender()
*/
@RequiresApi(Build.VERSION_CODES.O)
class FrameBufferRenderer(
@@ -121,7 +162,7 @@
/**
* Obtain a [FrameBuffer] to render content into. The [FrameBuffer] obtained here
* is expected to be managed by the consumer of [FrameBufferRenderer]. That is
- * callers of this API are expected to be maintaining a reference to the returned
+ * implementations of this API are expected to be maintaining a reference to the returned
* [FrameBuffer] here and calling [FrameBuffer.close] where appropriate as the instance
* will not be released by [FrameBufferRenderer].
*
@@ -131,7 +172,8 @@
fun obtainFrameBuffer(egl: EGLSpec): FrameBuffer
/**
- * Draw contents into the [HardwareBuffer]
+ * Draw contents into the [HardwareBuffer]. Before this method is invoked the [FrameBuffer]
+ * instance returned in [obtainFrameBuffer] is made current
*/
fun onDraw(eglManager: EGLManager)
@@ -141,6 +183,7 @@
*
* @param frameBuffer [FrameBuffer] that content is rendered into. The frameBuffer
* should not be consumed unless the syncFenceCompat is signalled or the fence is null.
+ * This is the same [FrameBuffer] instance returned in [obtainFrameBuffer]
* @param syncFenceCompat [SyncFenceCompat] is used to determine when rendering
* is done in [onDraw] and reflected within the given frameBuffer.
*/
@@ -166,6 +209,7 @@
*
* @param eglSpec an [EGLSpec] object to dictate the version of EGL and make EGL calls.
*/
+ @RequiresApi(Build.VERSION_CODES.KITKAT)
fun createSyncFence(eglSpec: EGLSpec): SyncFenceCompat?
companion object {
@@ -174,64 +218,10 @@
*/
@JvmField
val ALWAYS = object : SyncStrategy {
+ @RequiresApi(Build.VERSION_CODES.KITKAT)
override fun createSyncFence(eglSpec: EGLSpec): SyncFenceCompat? {
- return eglSpec.createNativeSyncFence()
+ return SyncFenceCompat.createNativeSyncFence()
}
}
}
}
-
-/**
- * [SyncStrategy] implementation that optimizes for front buffered rendering use cases.
- * More specifically this attempts to avoid unnecessary synchronization overhead
- * wherever possible.
- *
- * This will always provide a fence if the corresponding layer transitions from
- * an invisible to a visible state. If the layer is already visible and front
- * buffer usage flags are support on the device, then no fence is provided. If this
- * flag is not supported, then a fence is created to ensure contents
- * are flushed to the single buffer.
- *
- * @param usageFlags usage flags that describe the [HardwareBuffer] that is used as the destination
- * for rendering content within [FrameBufferRenderer]. The usage flags can be obtained via
- * [HardwareBuffer.getUsage] or by passing in the same flags from [HardwareBuffer.create]
- */
-class FrontBufferSyncStrategy(
- usageFlags: Long
-) : SyncStrategy {
- private val supportsFrontBufferUsage = (usageFlags and HardwareBuffer.USAGE_FRONT_BUFFER) != 0L
- private var mFrontBufferVisible: Boolean = false
-
- /**
- * Tells whether the corresponding front buffer layer is visible in its current state or not.
- * Utilize this to dictate when a [SyncFenceCompat] will be created when using
- * [createSyncFence].
- */
- var isVisible
- get() = mFrontBufferVisible
- set(visibility) {
- mFrontBufferVisible = visibility
- }
-
- /**
- * Creates a [SyncFenceCompat] based on various conditions.
- * If the layer is changing from invisible to visible, a fence is provided.
- * If the layer is already visible and front buffer usage flag is supported on the device, then
- * no fence is provided.
- * If front buffer usage is not supported, then a fence is created and destroyed to flush
- * contents to screen.
- */
- @RequiresApi(Build.VERSION_CODES.KITKAT)
- override fun createSyncFence(eglSpec: EGLSpec): SyncFenceCompat? {
- return if (!isVisible) {
- eglSpec.createNativeSyncFence()
- } else if (supportsFrontBufferUsage) {
- GLES20.glFlush()
- return null
- } else {
- val fence = eglSpec.createNativeSyncFence()
- fence.close()
- return null
- }
- }
-}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
index ed18a5b..dbd53cf 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
@@ -211,6 +211,16 @@
}
/**
+ * Queue a [Runnable] to be executed on the GL rendering thread. Note it is important that this
+ * [Runnable] does not block otherwise it can stall the GL thread.
+ *
+ * @param runnable Runnable to be executed
+ */
+ fun execute(runnable: Runnable) {
+ mGLThread?.execute(runnable)
+ }
+
+ /**
* Stop the corresponding GL thread. This destroys all EGLSurfaces as well
* as any other EGL dependencies. All queued requests that have not been processed
* yet are cancelled.
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt
index acde030..d39a39f 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt
@@ -134,6 +134,9 @@
}
}
+ @AnyThread
+ fun execute(runnable: Runnable) = withHandler { post(runnable) }
+
/**
* Removes the corresponding [android.view.Surface] from management of the GLThread.
* This destroys the EGLSurface associated with this surface and subsequent requests
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
index 7efd824..53c24d27 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
@@ -24,7 +24,6 @@
import android.os.Build
import android.view.Surface
import androidx.annotation.RequiresApi
-import androidx.hardware.SyncFence
import androidx.opengl.EGLExt
import androidx.opengl.EGLExt.Companion.EGLClientWaitResult
import androidx.opengl.EGLExt.Companion.EGLSyncAttribute
@@ -315,27 +314,6 @@
fun eglDestroySyncKHR(sync: EGLSyncKHR): Boolean
/**
- * Creates a native synchronization fence referenced through a file descriptor
- * that is associated with an EGL fence sync object.
- *
- * See:
- * https://www.khronos.org/registry/EGL/extensions/ANDROID/EGL_ANDROID_native_fence_sync.txt
- *
- * @param sync The [EGLSyncKHR] to fetch the [SyncFence] from
- * @return A [SyncFence] representing the native fence.
- * If [sync] is not a valid sync object for display, an invalid [SyncFence]
- * instance is returned and an EGL_BAD_PARAMETER error is generated.
- * If the EGL_SYNC_NATIVE_FENCE_FD_ANDROID attribute of [sync] is
- * EGL_NO_NATIVE_FENCE_FD_ANDROID, an invalid [SyncFence] is
- * returned and an EGL_BAD_PARAMETER error is generated.
- * If the display does not match the display passed to [eglCreateSyncKHR]
- * when [sync] was created, the behavior is undefined.
- */
- @Suppress("AcronymName")
- @RequiresApi(Build.VERSION_CODES.KITKAT)
- fun eglDupNativeFenceFDANDROID(sync: EGLSyncKHR): SyncFence
-
- /**
* Blocks the calling thread until the specified sync object is signalled or until
* [timeoutNanos] nanoseconds have passed.
* More than one [eglClientWaitSyncKHR] may be outstanding on the same [sync] at any given
@@ -533,10 +511,6 @@
override fun eglGetError(): Int = EGL14.eglGetError()
- @RequiresApi(Build.VERSION_CODES.KITKAT)
- override fun eglDupNativeFenceFDANDROID(sync: EGLSyncKHR): SyncFence =
- EGLExt.eglDupNativeFenceFDANDROID(getDefaultDisplay(), sync)
-
override fun eglClientWaitSyncKHR(
sync: EGLSyncKHR,
flags: Int,
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 9a6fc65..5b51639 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
@@ -26,7 +26,7 @@
import android.view.SurfaceView
import androidx.annotation.IntDef
import androidx.annotation.RequiresApi
-import androidx.graphics.lowlatency.SyncFenceCompat
+import androidx.hardware.SyncFenceCompat
import java.util.concurrent.Executor
/**
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 1c7810c..ec56717 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
@@ -25,8 +25,8 @@
import android.view.SurfaceControl
import android.view.SurfaceView
import androidx.annotation.RequiresApi
-import androidx.graphics.lowlatency.SyncFenceCompat
-import androidx.graphics.lowlatency.SyncFenceImpl
+import androidx.hardware.SyncFenceCompat
+import androidx.hardware.SyncFenceImpl
import androidx.graphics.surface.SurfaceControlCompat.TransactionCommittedListener
import java.util.concurrent.Executor
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 8605825..010d20c 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
@@ -24,11 +24,10 @@
import android.view.SurfaceView
import androidx.annotation.RequiresApi
import androidx.graphics.lowlatency.BufferTransformHintResolver.Companion.UNKNOWN_TRANSFORM
-import androidx.graphics.lowlatency.SyncFenceImpl
-import androidx.graphics.lowlatency.SyncFenceV19
+import androidx.hardware.SyncFenceImpl
import androidx.graphics.surface.SurfaceControlCompat.Companion.BUFFER_TRANSFORM_ROTATE_270
import androidx.graphics.surface.SurfaceControlCompat.Companion.BUFFER_TRANSFORM_ROTATE_90
-import androidx.hardware.SyncFence
+import androidx.hardware.SyncFenceV19
import java.util.concurrent.Executor
/**
@@ -379,9 +378,9 @@
)
}
- private fun SyncFenceImpl.asSyncFenceCompat(): SyncFence =
+ private fun SyncFenceImpl.asSyncFenceCompat(): SyncFenceV19 =
if (this is SyncFenceV19) {
- mSyncFence
+ this
} else {
throw IllegalArgumentException(
"Expected SyncFenceCompat implementation " +
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 03ecbc1..69e3f4a0 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
@@ -25,8 +25,8 @@
import android.view.SurfaceControl
import android.view.SurfaceView
import androidx.annotation.RequiresApi
-import androidx.graphics.lowlatency.SyncFenceImpl
-import androidx.graphics.lowlatency.SyncFenceV33
+import androidx.hardware.SyncFenceImpl
+import androidx.hardware.SyncFenceV33
import java.util.concurrent.Executor
/**
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt
index 042a484..6b48262 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt
@@ -23,7 +23,7 @@
import android.view.Surface
import android.view.SurfaceControl
import androidx.annotation.RequiresApi
-import androidx.hardware.SyncFence
+import androidx.hardware.SyncFenceV19
import java.util.concurrent.Executor
internal class JniBindings {
@@ -62,7 +62,7 @@
@JvmStatic
external fun nDupFenceFd(
- syncFence: SyncFence
+ syncFence: SyncFenceV19
): Int
@JvmStatic
@@ -70,7 +70,7 @@
surfaceTransaction: Long,
surfaceControl: Long,
hardwareBuffer: HardwareBuffer,
- acquireFieldFd: SyncFence
+ acquireFieldFd: SyncFenceV19
)
@JvmStatic
@@ -268,7 +268,7 @@
/**
* Updates the [HardwareBuffer] displayed for the provided surfaceControl. Takes an
- * optional [SyncFence] that is signalled when all pending work for the buffer
+ * optional [SyncFenceV19] that is signalled when all pending work for the buffer
* is complete and the buffer can be safely read.
*
* The frameworks takes ownership of the syncFence passed and is responsible for closing
@@ -289,7 +289,7 @@
fun setBuffer(
surfaceControl: SurfaceControlWrapper,
hardwareBuffer: HardwareBuffer,
- syncFence: SyncFence = SyncFence(-1)
+ syncFence: SyncFenceV19 = SyncFenceV19(-1)
): Transaction {
JniBindings.nSetBuffer(
mNativeSurfaceTransaction,
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceCompat.kt b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceCompat.kt
similarity index 78%
rename from graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceCompat.kt
rename to graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceCompat.kt
index 6f3bac4..5465eb8 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceCompat.kt
+++ b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceCompat.kt
@@ -14,18 +14,16 @@
* limitations under the License.
*/
-package androidx.graphics.lowlatency
+package androidx.hardware
import android.opengl.EGL14
import android.opengl.EGL15
import android.opengl.GLES20
import android.os.Build
import androidx.annotation.RequiresApi
-import androidx.graphics.opengl.egl.EGLSpec
-import androidx.hardware.SyncFence
import androidx.opengl.EGLExt
-import androidx.opengl.EGLSyncKHR
import androidx.graphics.surface.SurfaceControlCompat
+import androidx.opengl.EGLSyncKHR
/**
* A synchronization primitive which signals when hardware units have completed work on a
@@ -34,7 +32,7 @@
*
* [SyncFenceCompat] is a presentation fence used in combination with
* [SurfaceControlCompat.Transaction.setBuffer]. Note that depending on API level, this will
- * utilize either [android.hardware.SyncFence] or [SyncFence].
+ * utilize either [android.hardware.SyncFence] or a compatibility implementation.
*/
@RequiresApi(Build.VERSION_CODES.KITKAT)
class SyncFenceCompat : AutoCloseable {
@@ -44,21 +42,22 @@
/**
* Creates a native synchronization fence from an EGLSync object.
*
- * @param egl an [EGLSpec] object to dictate the version of EGL and make EGL calls.
- *
- * @throws IllegalArgumentException if sync object creation fails.
+ * @throws IllegalStateException if EGL dependencies cannot be resolved
*/
@JvmStatic
- fun createNativeSyncFence(egl: EGLSpec): SyncFenceCompat {
+ fun createNativeSyncFence(): SyncFenceCompat {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
SyncFenceCompatVerificationHelper.createSyncFenceCompatV33()
} else {
+ val display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
+ ?: throw IllegalStateException("No EGL Display available")
val eglSync: EGLSyncKHR =
- egl.eglCreateSyncKHR(EGLExt.EGL_SYNC_NATIVE_FENCE_ANDROID, null)
- ?: throw IllegalArgumentException("Unable to create sync object")
+ EGLExt.eglCreateSyncKHR(display, EGLExt.EGL_SYNC_NATIVE_FENCE_ANDROID, null)
+ ?: throw IllegalStateException("Unable to create sync object")
GLES20.glFlush()
- val syncFenceCompat = SyncFenceCompat(egl.eglDupNativeFenceFDANDROID(eglSync))
- egl.eglDestroySyncKHR(eglSync)
+
+ val syncFenceCompat = EGLExt.eglDupNativeFenceFDANDROID(display, eglSync)
+ EGLExt.eglDestroySyncKHR(display, eglSync)
syncFenceCompat
}
@@ -78,8 +77,8 @@
const val SIGNAL_TIME_PENDING: Long = Long.MAX_VALUE
}
- internal constructor(syncFence: SyncFence) {
- mImpl = SyncFenceV19(syncFence)
+ internal constructor(syncFence: SyncFenceV19) {
+ mImpl = syncFence
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@@ -110,8 +109,8 @@
/**
* Returns the time that the fence signaled in the [CLOCK_MONOTONIC] time domain.
- * This returns an instant, [SyncFence.SIGNAL_TIME_INVALID] if the SyncFence is invalid, and
- * if the fence hasn't yet signaled, then [SyncFence.SIGNAL_TIME_PENDING] is returned.
+ * This returns an instant, [SyncFenceCompat.SIGNAL_TIME_INVALID] if the SyncFence is invalid, and
+ * if the fence hasn't yet signaled, then [SyncFenceCompat.SIGNAL_TIME_PENDING] is returned.
*/
@RequiresApi(Build.VERSION_CODES.O)
fun getSignalTimeNanos(): Long {
@@ -126,15 +125,6 @@
}
/**
- * Creates a native synchronization fence from an EGLSync object.
- *
- * @throws IllegalArgumentException if sync object creation fails.
- */
-@RequiresApi(Build.VERSION_CODES.KITKAT)
-@JvmSynthetic
-fun EGLSpec.createNativeSyncFence(): SyncFenceCompat = SyncFenceCompat.createNativeSyncFence(this)
-
-/**
* Helper class to avoid class verification failures
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceImpl.kt b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceImpl.kt
similarity index 84%
rename from graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceImpl.kt
rename to graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceImpl.kt
index 27c0c25..a460188 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceImpl.kt
+++ b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceImpl.kt
@@ -14,11 +14,10 @@
* limitations under the License.
*/
-package androidx.graphics.lowlatency
+package androidx.hardware
import android.os.Build
import androidx.annotation.RequiresApi
-import androidx.hardware.SyncFence
internal interface SyncFenceImpl {
/**
@@ -40,8 +39,8 @@
/**
* Returns the time that the fence signaled in the [CLOCK_MONOTONIC] time domain.
- * This returns an instant, [SyncFence.SIGNAL_TIME_INVALID] if the SyncFence is invalid, and
- * if the fence hasn't yet signaled, then [SyncFence.SIGNAL_TIME_PENDING] is returned.
+ * This returns an instant, [SyncFenceCompat.SIGNAL_TIME_INVALID] if the SyncFence is invalid,
+ * and if the fence hasn't yet signaled, then [SyncFenceCompat.SIGNAL_TIME_PENDING] is returned.
*/
@RequiresApi(Build.VERSION_CODES.O)
fun getSignalTimeNanos(): Long
diff --git a/graphics/graphics-core/src/main/java/androidx/hardware/SyncFence.kt b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceV19.kt
similarity index 82%
rename from graphics/graphics-core/src/main/java/androidx/hardware/SyncFence.kt
rename to graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceV19.kt
index 4eac9f5..108bb14 100644
--- a/graphics/graphics-core/src/main/java/androidx/hardware/SyncFence.kt
+++ b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceV19.kt
@@ -33,7 +33,7 @@
* such as for display or media encoding.
*/
@RequiresApi(Build.VERSION_CODES.KITKAT)
-class SyncFence(private var fd: Int) : AutoCloseable {
+internal class SyncFenceV19(private var fd: Int) : AutoCloseable, SyncFenceImpl {
private val fenceLock = ReentrantLock()
@@ -41,21 +41,21 @@
* Checks if the SyncFence object is valid.
* @return `true` if it is valid, `false` otherwise
*/
- fun isValid(): Boolean = fenceLock.withLock {
+ override fun isValid(): Boolean = fenceLock.withLock {
fd != -1
}
/**
* Returns the time that the fence signaled in the [CLOCK_MONOTONIC] time domain.
- * This returns [SyncFence.SIGNAL_TIME_INVALID] if the SyncFence is invalid.
+ * This returns [SyncFenceCompat.SIGNAL_TIME_INVALID] if the SyncFence is invalid.
*/
// Relies on NDK APIs sync_file_info/sync_file_info_free which were introduced in API level 26
@RequiresApi(Build.VERSION_CODES.O)
- fun getSignalTime(): Long = fenceLock.withLock {
+ override fun getSignalTimeNanos(): Long = fenceLock.withLock {
if (isValid()) {
nGetSignalTime(fd)
} else {
- SIGNAL_TIME_INVALID
+ SyncFenceCompat.SIGNAL_TIME_INVALID
}
}
@@ -77,7 +77,7 @@
* indefinitely until the fence is signaled
* @return `true` if the fence signaled or is not valid, `false` otherwise
*/
- fun await(timeoutNanos: Long): Boolean {
+ override fun await(timeoutNanos: Long): Boolean {
fenceLock.withLock {
if (isValid()) {
val timeout: Int
@@ -100,7 +100,7 @@
*
* @return `true` if the fence signaled or isn't valid, `false` otherwise
*/
- fun awaitForever(): Boolean = await(-1)
+ override fun awaitForever(): Boolean = await(-1)
/**
* Close the SyncFence instance. After this method is invoked the fence is invalid. That
@@ -134,19 +134,6 @@
companion object {
- /**
- * An invalid signal time. Represents either the signal time for a SyncFence that isn't
- * valid (that is, [isValid] is `false`), or if an error occurred while attempting to
- * retrieve the signal time.
- */
- const val SIGNAL_TIME_INVALID: Long = -1L
-
- /**
- * A pending signal time. This is equivalent to the max value of a long, representing an
- * infinitely far point in the future.
- */
- const val SIGNAL_TIME_PENDING: Long = Long.MAX_VALUE
-
init {
System.loadLibrary("graphics-core")
}
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceV33.kt b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceV33.kt
similarity index 97%
rename from graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceV33.kt
rename to graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceV33.kt
index bab7753..86bc660f 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SyncFenceV33.kt
+++ b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFenceV33.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.graphics.lowlatency
+package androidx.hardware
import android.hardware.SyncFence
import android.os.Build
diff --git a/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt b/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt
index c124d19..3d2962a 100644
--- a/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt
+++ b/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt
@@ -21,7 +21,8 @@
import androidx.annotation.IntDef
import androidx.annotation.RequiresApi
import androidx.graphics.opengl.egl.EGLConfigAttributes
-import androidx.hardware.SyncFence
+import androidx.hardware.SyncFenceCompat
+import androidx.hardware.SyncFenceV19
import androidx.opengl.EGLExt.Companion.eglCreateSyncKHR
/**
@@ -561,12 +562,12 @@
* https://www.khronos.org/registry/EGL/extensions/ANDROID/EGL_ANDROID_native_fence_sync.txt
*
* @param display The EGLDisplay connection
- * @param sync The EGLSyncKHR to fetch the [SyncFence] from
- * @return A [SyncFence] representing the native fence.
- * If [sync] is not a valid sync object for [display], an invalid [SyncFence]
+ * @param sync The EGLSyncKHR to fetch the [SyncFenceCompat] from
+ * @return A [SyncFenceCompat] representing the native fence.
+ * If [sync] is not a valid sync object for [display], an invalid [SyncFenceCompat]
* instance is returned and an EGL_BAD_PARAMETER error is generated.
* If the EGL_SYNC_NATIVE_FENCE_FD_ANDROID attribute of [sync] is
- * EGL_NO_NATIVE_FENCE_FD_ANDROID, an invalid [SyncFence] is
+ * EGL_NO_NATIVE_FENCE_FD_ANDROID, an invalid [SyncFenceCompat] is
* returned and an EGL_BAD_PARAMETER error is generated.
* If [display] does not match the display passed to [eglCreateSyncKHR]
* when [sync] was created, the behavior is undefined.
@@ -574,15 +575,18 @@
@JvmStatic
@Suppress("AcronymName")
@RequiresApi(Build.VERSION_CODES.KITKAT)
- fun eglDupNativeFenceFDANDROID(display: EGLDisplay, sync: EGLSyncKHR): SyncFence {
+ internal fun eglDupNativeFenceFDANDROID(
+ display: EGLDisplay,
+ sync: EGLSyncKHR
+ ): SyncFenceCompat {
val fd = EGLBindings.nDupNativeFenceFDANDROID(
display.obtainNativeHandle(),
sync.nativeHandle
)
return if (fd >= 0) {
- SyncFence(fd)
+ SyncFenceCompat(SyncFenceV19(fd))
} else {
- SyncFence(-1)
+ SyncFenceCompat(SyncFenceV19(-1))
}
}
diff --git a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonTest.kt b/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonTest.kt
index 379f501..69601cf 100644
--- a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonTest.kt
+++ b/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonTest.kt
@@ -21,7 +21,10 @@
import androidx.core.graphics.plus
import androidx.core.graphics.times
import androidx.test.filters.SmallTest
-import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
import org.junit.Test
@SmallTest
@@ -73,7 +76,7 @@
fun pathTest() {
val shape = square.toCubicShape()
val path = shape.toPath()
- Assert.assertFalse(path.isEmpty)
+ assertFalse(path.isEmpty)
}
@Test
@@ -95,7 +98,7 @@
val squareCopy = Polygon(square)
val identity = Matrix()
square.transform(identity)
- Assert.assertEquals(square, squareCopy)
+ assertEquals(square, squareCopy)
// Now create a matrix which translates points by (1, 2) and make sure
// the shape is translated similarly by it
@@ -112,4 +115,48 @@
assertPointsEqualish(squareCopyCubics[i].p3 + offset, squareCubics[i].p3)
}
}
+
+ @Test
+ fun featuresTest() {
+ val squareFeatures = square.features
+
+ // Verify that cubics of polygon == cubics of features of that polygon
+ assertTrue(square.toCubicShape().cubics == squareFeatures.flatMap { it.cubics })
+
+ // Same as above but with rounded corners
+ val roundedSquare = RoundedPolygon(4, rounding = CornerRounding(.1f))
+ val roundedFeatures = roundedSquare.features
+ assertTrue(roundedSquare.toCubicShape().cubics == roundedFeatures.flatMap { it.cubics })
+
+ // Same as the first polygon test, but with a copy of that polygon
+ val squareCopy = Polygon(square)
+ val squareCopyFeatures = squareCopy.features
+ assertTrue(squareCopy.toCubicShape().cubics == squareCopyFeatures.flatMap { it.cubics })
+
+ // Test other elements of Features
+ val copy = Polygon(square)
+ val matrix = Matrix()
+ matrix.setTranslate(1f, 2f)
+ val features = copy.features
+ val preTransformVertices = mutableListOf<PointF>()
+ val preTransformCenters = mutableListOf<PointF>()
+ for (feature in features) {
+ if (feature is Corner) {
+ // Copy into new Point objects since the ones in the feature should transform
+ preTransformVertices.add(PointF(feature.vertex.x, feature.vertex.y))
+ preTransformCenters.add(PointF(feature.roundedCenter.x, feature.roundedCenter.y))
+ }
+ }
+ copy.transform(matrix)
+ val postTransformVertices = mutableListOf<PointF>()
+ val postTransformCenters = mutableListOf<PointF>()
+ for (feature in features) {
+ if (feature is Corner) {
+ postTransformVertices.add(feature.vertex)
+ postTransformCenters.add(feature.roundedCenter)
+ }
+ }
+ assertNotEquals(preTransformVertices, postTransformVertices)
+ assertNotEquals(preTransformCenters, postTransformCenters)
+ }
}
\ No newline at end of file
diff --git a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Polygon.kt b/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Polygon.kt
index 24194e4..5129044 100644
--- a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Polygon.kt
+++ b/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Polygon.kt
@@ -181,11 +181,12 @@
val tempFeatures = mutableListOf<Feature>()
for (feature in source.features) {
if (feature is Edge) {
- tempFeatures.add(Edge(feature))
+ tempFeatures.add(Edge(this, feature))
} else {
- tempFeatures.add(Corner(feature as Corner))
+ tempFeatures.add(Corner(this, feature as Corner))
}
}
+ features = tempFeatures
center = PointF(source.center.x, source.center.y)
cubicShape.updateCubics(newCubics)
}
@@ -257,11 +258,15 @@
// from above, along with new cubics representing the edges between those corners.
val tempFeatures = mutableListOf<Feature>()
for (i in 0 until n) {
- cubics.addAll(corners[i])
+ val cornerIndices = mutableListOf<Int>()
+ for (cubic in corners[i]) {
+ cornerIndices.add(cubics.size)
+ cubics.add(cubic)
+ }
// TODO: determine and pass convexity flag
- tempFeatures.add(Corner(corners[i], roundedCorners[i].center, vertices[i]))
+ tempFeatures.add(Corner(this, cornerIndices, roundedCorners[i].center, vertices[i]))
+ tempFeatures.add(Edge(this, listOf(cubics.size)))
cubics.add(Cubic.straightLine(corners[i].last().p3, corners[(i + 1) % n].first().p0))
- tempFeatures.add(Edge(listOf(cubics[cubics.size - 1])))
}
features = tempFeatures
cubicShape.updateCubics(cubics)
@@ -276,6 +281,9 @@
matrix.mapPoints(point)
center.x = point[0]
center.y = point[1]
+ for (feature in features) {
+ feature.transform(matrix)
+ }
}
/**
@@ -415,15 +423,27 @@
* of what the shape actually is, rather than simply manipulating the raw curves and lines
* which describe it.
*/
-internal sealed class Feature(val cubics: List<Cubic>)
+internal sealed class Feature(private val polygon: Polygon, protected val cubicIndices: List<Int>) {
+ val cubics: MutableList<Cubic>
+ get() {
+ val featureCubics = mutableListOf<Cubic>()
+ val cubics = polygon.toCubicShape().cubics
+ for (index in cubicIndices) {
+ featureCubics.add(cubics[index])
+ }
+ return featureCubics
+ }
+
+ open fun transform(matrix: Matrix) {}
+}
/**
* Edges have only a list of the cubic curves which make up the edge. Edges lie between
* corners and have no vertex or concavity; the curves are simply straight lines (represented
* by Cubic curves).
*/
-internal class Edge(cubics: List<Cubic>) : Feature(cubics) {
- constructor(source: Edge) : this(source.cubics)
+internal class Edge(polygon: Polygon, indices: List<Int>) : Feature(polygon, indices) {
+ constructor(polygon: Polygon, source: Edge) : this(polygon, source.cubicIndices)
}
/**
@@ -434,18 +454,27 @@
* convex (outer) and concave (inner) corners.
*/
internal class Corner(
- cubics: List<Cubic>,
+ polygon: Polygon,
+ cubicIndices: List<Int>,
// TODO: parameters here should be immutable
val vertex: PointF,
val roundedCenter: PointF,
val convex: Boolean = true
-) : Feature(cubics) {
- constructor(source: Corner) : this(
- source.cubics,
+) : Feature(polygon, cubicIndices) {
+ constructor(polygon: Polygon, source: Corner) : this(
+ polygon,
+ source.cubicIndices,
source.vertex,
source.roundedCenter,
source.convex
)
+
+ override fun transform(matrix: Matrix) {
+ val tempPoints = floatArrayOf(vertex.x, vertex.y, roundedCenter.x, roundedCenter.y)
+ matrix.mapPoints(tempPoints)
+ vertex.set(tempPoints[0], tempPoints[1])
+ roundedCenter.set(tempPoints[2], tempPoints[3])
+ }
}
/**
diff --git a/health/connect/connect-client-proto/src/main/proto/data.proto b/health/connect/connect-client-proto/src/main/proto/data.proto
index 2ccddb3..b5e10a0 100644
--- a/health/connect/connect-client-proto/src/main/proto/data.proto
+++ b/health/connect/connect-client-proto/src/main/proto/data.proto
@@ -50,6 +50,13 @@
optional int64 instant_time_millis = 2;
}
+message SubTypeDataValue {
+ map<string, Value> values = 1;
+ optional int64 start_time_millis = 2;
+ optional int64 end_time_millis = 3;
+}
+
+// Next Id: 22
message DataPoint {
optional DataType data_type = 1;
map<string, Value> values = 2;
@@ -82,6 +89,12 @@
optional int32 start_zone_offset_seconds = 19;
optional int32 end_zone_offset_seconds = 20;
+
+ optional SubTypeDataValue sub_type_data_values = 21;
+
+ message SubTypeDataList {
+ repeated SubTypeDataValue values = 1;
+ }
}
message AggregatedValue {
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 b466452..54e26a1 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
@@ -359,6 +359,10 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal const val DEFAULT_PROVIDER_PACKAGE_NAME = "com.google.android.apps.healthdata"
+ @RestrictTo(RestrictTo.Scope.LIBRARY) // To be released after testing
+ const val HEALTH_CONNECT_SETTING_INTENT_ACTION =
+ "androidx.health.ACTION_HEALTH_CONNECT_SETTINGS"
+
/**
* 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
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/SdkConfig.java b/health/connect/connect-client/src/main/java/androidx/health/platform/client/SdkConfig.java
index 3e2ec2d..7629a30 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/platform/client/SdkConfig.java
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/SdkConfig.java
@@ -25,7 +25,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
public final class SdkConfig {
// should be increased everytime a new SDK is released
- public static final int SDK_VERSION = 10;
+ public static final int SDK_VERSION = 11;
private SdkConfig() {}
}
diff --git a/libraryversions.toml b/libraryversions.toml
index c7b23ba..c22b486 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -21,7 +21,7 @@
COLLECTION = "1.3.0-alpha03"
COLLECTION_KMP = "1.3.0-dev01"
COMPOSE = "1.4.0-alpha06"
-COMPOSE_COMPILER = "1.4.0"
+COMPOSE_COMPILER = "1.4.1"
COMPOSE_MATERIAL3 = "1.1.0-alpha05"
COMPOSE_RUNTIME_TRACING = "1.0.0-alpha02"
CONSTRAINTLAYOUT = "2.2.0-alpha07"
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
index c067c51..3b02e84 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
@@ -270,6 +270,6 @@
val store = ViewModelStore()
val factory = FakeViewModelProviderFactory()
- override fun getViewModelStore(): ViewModelStore = store
+ override val viewModelStore: ViewModelStore = store
override val defaultViewModelProviderFactory = factory
}
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
index 3ceb0b3..a88aecc 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
@@ -52,10 +52,6 @@
constraints {
implementation(project(":lifecycle:lifecycle-livedata-core"))
implementation(project(":lifecycle:lifecycle-viewmodel"))
- // this syntax is a temporary workout to allow project dependency on cross-project-set
- // i.e. COMPOSE + MAIN project sets
- // update syntax when b/239979823 is fixed
- implementation("androidx.lifecycle:lifecycle-viewmodel-compose:${androidx.LibraryVersions.LIFECYCLE}")
}
}
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/TestComponent.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/TestComponent.kt
index 1f7b190..efdb532 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/TestComponent.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/TestComponent.kt
@@ -49,7 +49,7 @@
override val savedStateRegistry: SavedStateRegistry
get() = savedStateController.savedStateRegistry
- override fun getViewModelStore(): ViewModelStore = vmStore
+ override val viewModelStore: ViewModelStore = vmStore
fun resume() {
lifecycleRegistry.currentState = Lifecycle.State.RESUMED
diff --git a/lifecycle/lifecycle-viewmodel/api/current.txt b/lifecycle/lifecycle-viewmodel/api/current.txt
index 75944f4..f8457f6 100644
--- a/lifecycle/lifecycle-viewmodel/api/current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/current.txt
@@ -80,6 +80,7 @@
public interface ViewModelStoreOwner {
method public androidx.lifecycle.ViewModelStore getViewModelStore();
+ property public abstract androidx.lifecycle.ViewModelStore viewModelStore;
}
public final class ViewTreeViewModelKt {
diff --git a/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt
index 75944f4..f8457f6 100644
--- a/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt
@@ -80,6 +80,7 @@
public interface ViewModelStoreOwner {
method public androidx.lifecycle.ViewModelStore getViewModelStore();
+ property public abstract androidx.lifecycle.ViewModelStore viewModelStore;
}
public final class ViewTreeViewModelKt {
diff --git a/lifecycle/lifecycle-viewmodel/api/restricted_current.txt b/lifecycle/lifecycle-viewmodel/api/restricted_current.txt
index 75944f4..f8457f6 100644
--- a/lifecycle/lifecycle-viewmodel/api/restricted_current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/restricted_current.txt
@@ -80,6 +80,7 @@
public interface ViewModelStoreOwner {
method public androidx.lifecycle.ViewModelStore getViewModelStore();
+ property public abstract androidx.lifecycle.ViewModelStore viewModelStore;
}
public final class ViewTreeViewModelKt {
diff --git a/lifecycle/lifecycle-viewmodel/src/androidTest/java/androidx/lifecycle/ViewTreeViewModelStoreOwnerTest.kt b/lifecycle/lifecycle-viewmodel/src/androidTest/java/androidx/lifecycle/ViewTreeViewModelStoreOwnerTest.kt
index 2fd80c7..6424d49 100644
--- a/lifecycle/lifecycle-viewmodel/src/androidTest/java/androidx/lifecycle/ViewTreeViewModelStoreOwnerTest.kt
+++ b/lifecycle/lifecycle-viewmodel/src/androidTest/java/androidx/lifecycle/ViewTreeViewModelStoreOwnerTest.kt
@@ -133,8 +133,9 @@
}
internal class FakeViewModelStoreOwner : ViewModelStoreOwner {
- override fun getViewModelStore(): ViewModelStore {
- throw UnsupportedOperationException("not a real ViewModelStoreOwner")
- }
+ override val viewModelStore: ViewModelStore
+ get() {
+ throw UnsupportedOperationException("not a real ViewModelStoreOwner")
+ }
}
}
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelStoreOwner.java b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelStoreOwner.kt
similarity index 64%
rename from lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelStoreOwner.java
rename to lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelStoreOwner.kt
index 19152be..5296800 100644
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelStoreOwner.java
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelStoreOwner.kt
@@ -13,27 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-package androidx.lifecycle;
-
-import androidx.annotation.NonNull;
+package androidx.lifecycle
/**
- * A scope that owns {@link ViewModelStore}.
- * <p>
+ * A scope that owns [ViewModelStore].
+ *
* A responsibility of an implementation of this interface is to retain owned ViewModelStore
- * during the configuration changes and call {@link ViewModelStore#clear()}, when this scope is
+ * during the configuration changes and call [ViewModelStore.clear], when this scope is
* going to be destroyed.
*
* @see ViewTreeViewModelStoreOwner
*/
-@SuppressWarnings("WeakerAccess")
-public interface ViewModelStoreOwner {
+interface ViewModelStoreOwner {
+
/**
- * Returns owned {@link ViewModelStore}
- *
- * @return a {@code ViewModelStore}
+ * The owned [ViewModelStore]
*/
- @NonNull
- ViewModelStore getViewModelStore();
-}
+ val viewModelStore: ViewModelStore
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt b/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt
index 2536918..934878d 100644
--- a/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt
+++ b/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt
@@ -62,8 +62,7 @@
@Test
fun testOwnedBy() {
- val store = ViewModelStore()
- val owner = ViewModelStoreOwner { store }
+ val owner = FakeViewModelStoreOwner()
val provider = ViewModelProvider(owner, ViewModelProvider.NewInstanceFactory())
val viewModel = provider[ViewModel1::class.java]
assertThat(viewModel).isSameInstanceAs(provider[ViewModel1::class.java])
@@ -82,8 +81,7 @@
@Test
fun testKeyedFactory() {
- val store = ViewModelStore()
- val owner = ViewModelStoreOwner { store }
+ val owner = FakeViewModelStoreOwner()
val explicitlyKeyed: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(
modelClass: Class<T>,
@@ -172,13 +170,16 @@
private val mStore: ViewModelStore,
private val mFactory: ViewModelProvider.Factory
) : ViewModelStoreOwner, HasDefaultViewModelProviderFactory {
- override fun getViewModelStore(): ViewModelStore {
- return mStore
- }
-
+ override val viewModelStore: ViewModelStore = mStore
override val defaultViewModelProviderFactory = mFactory
}
+ class FakeViewModelStoreOwner internal constructor(
+ store: ViewModelStore = ViewModelStore()
+ ) : ViewModelStoreOwner {
+ override val viewModelStore: ViewModelStore = store
+ }
+
open class ViewModel1 : ViewModel() {
var cleared = false
override fun onCleared() {
@@ -197,7 +198,7 @@
internal open class ViewModelStoreOwnerWithCreationExtras : ViewModelStoreOwner,
HasDefaultViewModelProviderFactory {
- private val viewModelStore = ViewModelStore()
+ private val _viewModelStore = ViewModelStore()
override val defaultViewModelProviderFactory: ViewModelProvider.Factory
get() = throw UnsupportedOperationException()
@@ -208,9 +209,7 @@
return extras
}
- override fun getViewModelStore(): ViewModelStore {
- return viewModelStore
- }
+ override val viewModelStore: ViewModelStore = _viewModelStore
}
}
diff --git a/lint-checks/src/main/java/androidx/build/lint/CameraXQuirksClassDetector.kt b/lint-checks/src/main/java/androidx/build/lint/CameraXQuirksClassDetector.kt
index 842f9c3..7326529 100644
--- a/lint-checks/src/main/java/androidx/build/lint/CameraXQuirksClassDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/CameraXQuirksClassDetector.kt
@@ -20,6 +20,7 @@
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Incident
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope
@@ -38,7 +39,7 @@
override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
override fun visitClass(node: UClass) {
- val isQuirk = node.implementsList?.referenceElements?.find { it ->
+ val isQuirk = node.implementsList?.referenceElements?.find {
it.referenceName!!.endsWith("Quirk")
} != null
@@ -59,12 +60,13 @@
* Device(s):
""".trimIndent()
- context.report(
- CameraXQuirksClassDetector.ISSUE, node,
- context.getNameLocation(node),
- "CameraX quirks should include this template in the javadoc:" +
- "\n\n$implForInsertion\n\n"
- )
+ val incident = Incident(context)
+ .issue(ISSUE)
+ .message("CameraX quirks should include this template in the javadoc:" +
+ "\n\n$implForInsertion\n\n")
+ .location(context.getNameLocation(node))
+ .scope(node)
+ context.report(incident)
}
}
}
diff --git a/navigation/navigation-common/api/current.txt b/navigation/navigation-common/api/current.txt
index 34e00ed..a0905df 100644
--- a/navigation/navigation-common/api/current.txt
+++ b/navigation/navigation-common/api/current.txt
@@ -129,6 +129,7 @@
property public final String id;
property public final androidx.lifecycle.SavedStateHandle savedStateHandle;
property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
+ property public androidx.lifecycle.ViewModelStore viewModelStore;
}
public final class NavDeepLink {
diff --git a/navigation/navigation-common/api/public_plus_experimental_current.txt b/navigation/navigation-common/api/public_plus_experimental_current.txt
index 34e00ed..a0905df 100644
--- a/navigation/navigation-common/api/public_plus_experimental_current.txt
+++ b/navigation/navigation-common/api/public_plus_experimental_current.txt
@@ -129,6 +129,7 @@
property public final String id;
property public final androidx.lifecycle.SavedStateHandle savedStateHandle;
property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
+ property public androidx.lifecycle.ViewModelStore viewModelStore;
}
public final class NavDeepLink {
diff --git a/navigation/navigation-common/api/restricted_current.txt b/navigation/navigation-common/api/restricted_current.txt
index 34e00ed..a0905df 100644
--- a/navigation/navigation-common/api/restricted_current.txt
+++ b/navigation/navigation-common/api/restricted_current.txt
@@ -129,6 +129,7 @@
property public final String id;
property public final androidx.lifecycle.SavedStateHandle savedStateHandle;
property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
+ property public androidx.lifecycle.ViewModelStore viewModelStore;
}
public final class NavDeepLink {
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
index 3f0656e..953aae9 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
@@ -197,29 +197,30 @@
}
}
- /**
- * {@inheritDoc}
- *
- * @throws IllegalStateException if called before the [lifecycle] has moved to
- * [Lifecycle.State.CREATED] or before the [androidx.navigation.NavHost] has called
- * [androidx.navigation.NavHostController.setViewModelStore].
- */
- public override fun getViewModelStore(): ViewModelStore {
- check(savedStateRegistryAttached) {
- "You cannot access the NavBackStackEntry's ViewModels until it is added to " +
- "the NavController's back stack (i.e., the Lifecycle of the NavBackStackEntry " +
- "reaches the CREATED state)."
+ public override val viewModelStore: ViewModelStore
+ /**
+ * {@inheritDoc}
+ *
+ * @throws IllegalStateException if called before the [lifecycle] has moved to
+ * [Lifecycle.State.CREATED] or before the [androidx.navigation.NavHost] has called
+ * [androidx.navigation.NavHostController.setViewModelStore].
+ */
+ get() {
+ check(savedStateRegistryAttached) {
+ "You cannot access the NavBackStackEntry's ViewModels until it is added to " +
+ "the NavController's back stack (i.e., the Lifecycle of the " +
+ "NavBackStackEntry reaches the CREATED state)."
+ }
+ check(lifecycle.currentState != Lifecycle.State.DESTROYED) {
+ "You cannot access the NavBackStackEntry's ViewModels after the " +
+ "NavBackStackEntry is destroyed."
+ }
+ checkNotNull(viewModelStoreProvider) {
+ "You must call setViewModelStore() on your NavHostController before " +
+ "accessing the ViewModelStore of a navigation graph."
+ }
+ return viewModelStoreProvider.getViewModelStore(id)
}
- check(lifecycle.currentState != Lifecycle.State.DESTROYED) {
- "You cannot access the NavBackStackEntry's ViewModels after the " +
- "NavBackStackEntry is destroyed."
- }
- checkNotNull(viewModelStoreProvider) {
- "You must call setViewModelStore() on your NavHostController before accessing the " +
- "ViewModelStore of a navigation graph."
- }
- return viewModelStoreProvider.getViewModelStore(id)
- }
override val defaultViewModelProviderFactory: ViewModelProvider.Factory = defaultFactory
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index f653d40..597d61c 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -25,7 +25,7 @@
kotlin.code.style=official
# Disable docs
androidx.enableDocumentation=false
-androidx.playground.snapshotBuildId=9410467
-androidx.playground.metalavaBuildId=9370379
+androidx.playground.snapshotBuildId=9519019
+androidx.playground.metalavaBuildId=9508658
androidx.playground.dokkaBuildId=7472101
androidx.studio.type=playground
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java
index 247232a..49c545ae 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java
@@ -86,7 +86,7 @@
TopicsManagerFutures topicsManager =
TopicsManagerFutures.from(ApplicationProvider.getApplicationContext());
GetTopicsRequest request = new GetTopicsRequest.Builder()
- .setSdkName("sdk1")
+ .setAdsSdkName("sdk1")
.setShouldRecordObservation(true)
.build();
GetTopicsResponse response = topicsManager.getTopicsAsync(request).get();
@@ -124,7 +124,7 @@
// Sdk 2 did not call getTopics API. So it should not receive any topic.
GetTopicsResponse response2 = topicsManager.getTopicsAsync(
new GetTopicsRequest.Builder()
- .setSdkName("sdk2")
+ .setAdsSdkName("sdk2")
.build()).get();
assertThat(response2.getTopics()).isEmpty();
}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFuturesTest.kt
index 36ce0d6..9512955 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFuturesTest.kt
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFuturesTest.kt
@@ -78,7 +78,7 @@
// Actually invoke the compat code.
val request = GetTopicsRequest.Builder()
- .setSdkName(mSdkName)
+ .setAdsSdkName(mSdkName)
.setShouldRecordObservation(true)
.build()
diff --git a/privacysandbox/ads/ads-adservices/api/current.txt b/privacysandbox/ads/ads-adservices/api/current.txt
index 01aa5c6..30cd307 100644
--- a/privacysandbox/ads/ads-adservices/api/current.txt
+++ b/privacysandbox/ads/ads-adservices/api/current.txt
@@ -301,17 +301,17 @@
package androidx.privacysandbox.ads.adservices.topics {
public final class GetTopicsRequest {
- ctor public GetTopicsRequest(optional String sdkName, optional boolean shouldRecordObservation);
- method public String getSdkName();
+ ctor public GetTopicsRequest(optional String adsSdkName, optional boolean shouldRecordObservation);
+ method public String getAdsSdkName();
method public boolean getShouldRecordObservation();
- property public final String sdkName;
+ property public final String adsSdkName;
property public final boolean shouldRecordObservation;
}
public static final class GetTopicsRequest.Builder {
ctor public GetTopicsRequest.Builder();
method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest build();
- method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setSdkName(String sdkName);
+ method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setAdsSdkName(String adsSdkName);
method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setShouldRecordObservation(boolean shouldRecordObservation);
}
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 01aa5c6..30cd307 100644
--- a/privacysandbox/ads/ads-adservices/api/public_plus_experimental_current.txt
+++ b/privacysandbox/ads/ads-adservices/api/public_plus_experimental_current.txt
@@ -301,17 +301,17 @@
package androidx.privacysandbox.ads.adservices.topics {
public final class GetTopicsRequest {
- ctor public GetTopicsRequest(optional String sdkName, optional boolean shouldRecordObservation);
- method public String getSdkName();
+ ctor public GetTopicsRequest(optional String adsSdkName, optional boolean shouldRecordObservation);
+ method public String getAdsSdkName();
method public boolean getShouldRecordObservation();
- property public final String sdkName;
+ property public final String adsSdkName;
property public final boolean shouldRecordObservation;
}
public static final class GetTopicsRequest.Builder {
ctor public GetTopicsRequest.Builder();
method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest build();
- method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setSdkName(String sdkName);
+ method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setAdsSdkName(String adsSdkName);
method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setShouldRecordObservation(boolean shouldRecordObservation);
}
diff --git a/privacysandbox/ads/ads-adservices/api/restricted_current.txt b/privacysandbox/ads/ads-adservices/api/restricted_current.txt
index 01aa5c6..30cd307 100644
--- a/privacysandbox/ads/ads-adservices/api/restricted_current.txt
+++ b/privacysandbox/ads/ads-adservices/api/restricted_current.txt
@@ -301,17 +301,17 @@
package androidx.privacysandbox.ads.adservices.topics {
public final class GetTopicsRequest {
- ctor public GetTopicsRequest(optional String sdkName, optional boolean shouldRecordObservation);
- method public String getSdkName();
+ ctor public GetTopicsRequest(optional String adsSdkName, optional boolean shouldRecordObservation);
+ method public String getAdsSdkName();
method public boolean getShouldRecordObservation();
- property public final String sdkName;
+ property public final String adsSdkName;
property public final boolean shouldRecordObservation;
}
public static final class GetTopicsRequest.Builder {
ctor public GetTopicsRequest.Builder();
method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest build();
- method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setSdkName(String sdkName);
+ method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setAdsSdkName(String adsSdkName);
method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setShouldRecordObservation(boolean shouldRecordObservation);
}
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequestTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequestTest.kt
index a04b48d..297e54c 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequestTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequestTest.kt
@@ -27,13 +27,13 @@
class GetTopicsRequestTest {
@Test
fun testToString() {
- val result = "GetTopicsRequest: sdkName=sdk1, shouldRecordObservation=false"
+ val result = "GetTopicsRequest: adsSdkName=sdk1, shouldRecordObservation=false"
val request = GetTopicsRequest("sdk1", false)
Truth.assertThat(request.toString()).isEqualTo(result)
// Verify Builder.
val request2 = GetTopicsRequest.Builder()
- .setSdkName("sdk1")
+ .setAdsSdkName("sdk1")
.setShouldRecordObservation(false)
.build()
Truth.assertThat(request.toString()).isEqualTo(result)
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt
index 3619d38..80e915a 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt
@@ -79,7 +79,7 @@
// Actually invoke the compat code.
val result = runBlocking {
val request = GetTopicsRequest.Builder()
- .setSdkName(mSdkName)
+ .setAdsSdkName(mSdkName)
.setShouldRecordObservation(true)
.build()
@@ -108,7 +108,7 @@
val managerCompat = obtain(mContext)
val request = GetTopicsRequest.Builder()
- .setSdkName(mSdkName)
+ .setAdsSdkName(mSdkName)
.setShouldRecordObservation(false)
.build()
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
index ef41c4d..eaaae293 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
@@ -81,7 +81,8 @@
/**
* Creates [AdIdManager].
*
- * @return AdIdManager object.
+ * @return AdIdManager object. If the device is running an incompatible
+ * build, the value returned is null.
*/
@JvmStatic
@SuppressLint("NewApi", "ClassVerificationFailure")
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
index 844de9a..7c57149 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
@@ -77,7 +77,8 @@
/**
* Creates [AppSetIdManager].
*
- * @return AppSetIdManager object.
+ * @return AppSetIdManager object. If the device is running an incompatible
+ * build, the value returned is null.
*/
@JvmStatic
@SuppressLint("NewApi", "ClassVerificationFailure")
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
index d78c779..f41199b 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
@@ -203,7 +203,8 @@
/**
* Creates [CustomAudienceManager].
*
- * @return CustomAudienceManager object.
+ * @return CustomAudienceManager object. If the device is running an incompatible
+ * build, the value returned is null.
*/
@JvmStatic
@SuppressLint("NewApi", "ClassVerificationFailure")
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
index 3309244..ee47562 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
@@ -244,7 +244,8 @@
/**
* Creates [MeasurementManager].
*
- * @return MeasurementManager object.
+ * @return MeasurementManager object. If the device is running an incompatible
+ * build, the value returned is null.
*/
@JvmStatic
@SuppressLint("NewApi", "ClassVerificationFailure")
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequest.kt
index 1bfa0d37..af07f77 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequest.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequest.kt
@@ -20,31 +20,31 @@
* Represents the request for the getTopics API (which takes a [GetTopicsRequest] and
* returns a [GetTopicsResponse].
*
- * @param sdkName The Ads SDK name. This must be called by SDKs running outside of the Sandbox.
+ * @param adsSdkName The Ads SDK name. This must be called by SDKs running outside of the Sandbox.
* Other clients must not call it.
* @param shouldRecordObservation whether to record that the caller has observed the topics of the
* host app or not. This will be used to determine if the caller can receive the topic
* in the next epoch.
*/
class GetTopicsRequest public constructor(
- val sdkName: String = "",
+ val adsSdkName: String = "",
@get:JvmName("shouldRecordObservation")
val shouldRecordObservation: Boolean = false
) {
override fun toString(): String {
return "GetTopicsRequest: " +
- "sdkName=$sdkName, shouldRecordObservation=$shouldRecordObservation"
+ "adsSdkName=$adsSdkName, shouldRecordObservation=$shouldRecordObservation"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is GetTopicsRequest) return false
- return this.sdkName == other.sdkName &&
+ return this.adsSdkName == other.adsSdkName &&
this.shouldRecordObservation == other.shouldRecordObservation
}
override fun hashCode(): Int {
- var hash = sdkName.hashCode()
+ var hash = adsSdkName.hashCode()
hash = 31 * hash + shouldRecordObservation.hashCode()
return hash
}
@@ -53,7 +53,7 @@
* Builder for [GetTopicsRequest].
*/
public class Builder() {
- private var sdkName: String = ""
+ private var adsSdkName: String = ""
private var shouldRecordObservation: Boolean = true
/**
@@ -62,9 +62,9 @@
* <p>This must be called by SDKs running outside of the Sandbox. Other clients must not
* call it.
*
- * @param sdkName the Ads Sdk Name.
+ * @param adsSdkName the Ads Sdk Name.
*/
- fun setSdkName(sdkName: String): Builder = apply { this.sdkName = sdkName }
+ fun setAdsSdkName(adsSdkName: String): Builder = apply { this.adsSdkName = adsSdkName }
/**
* Set the Record Observation.
@@ -80,8 +80,8 @@
/** Builds a [GetTopicsRequest] instance. */
fun build(): GetTopicsRequest {
- check(sdkName.isNotEmpty()) { "sdkName must be set" }
- return GetTopicsRequest(sdkName, shouldRecordObservation)
+ check(adsSdkName.isNotEmpty()) { "adsSdkName must be set" }
+ return GetTopicsRequest(adsSdkName, shouldRecordObservation)
}
}
}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt
index 67d7764..488125d 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt
@@ -81,7 +81,7 @@
throw IllegalArgumentException("shouldRecordObservation not supported yet.")
}
return android.adservices.topics.GetTopicsRequest.Builder()
- .setAdsSdkName(request.sdkName)
+ .setAdsSdkName(request.adsSdkName)
.build()
}
diff --git a/privacysandbox/ui/ui-client/api/current.txt b/privacysandbox/ui/ui-client/api/current.txt
index e6f50d0..e6beb91 100644
--- a/privacysandbox/ui/ui-client/api/current.txt
+++ b/privacysandbox/ui/ui-client/api/current.txt
@@ -1 +1,10 @@
// Signature format: 4.0
+package androidx.privacysandbox.ui.client {
+
+ @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;
+ }
+
+}
+
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 e6f50d0..e6beb91 100644
--- a/privacysandbox/ui/ui-client/api/public_plus_experimental_current.txt
+++ b/privacysandbox/ui/ui-client/api/public_plus_experimental_current.txt
@@ -1 +1,10 @@
// Signature format: 4.0
+package androidx.privacysandbox.ui.client {
+
+ @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;
+ }
+
+}
+
diff --git a/privacysandbox/ui/ui-client/api/restricted_current.txt b/privacysandbox/ui/ui-client/api/restricted_current.txt
index e6f50d0..e6beb91 100644
--- a/privacysandbox/ui/ui-client/api/restricted_current.txt
+++ b/privacysandbox/ui/ui-client/api/restricted_current.txt
@@ -1 +1,10 @@
// Signature format: 4.0
+package androidx.privacysandbox.ui.client {
+
+ @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;
+ }
+
+}
+
diff --git a/privacysandbox/ui/ui-client/build.gradle b/privacysandbox/ui/ui-client/build.gradle
index 82beabb..918c043 100644
--- a/privacysandbox/ui/ui-client/build.gradle
+++ b/privacysandbox/ui/ui-client/build.gradle
@@ -24,7 +24,8 @@
dependencies {
api(libs.kotlinStdlib)
- // Add dependencies here
+ implementation project(path: ':privacysandbox:ui:ui-core')
+ implementation project(path: ':annotation:annotation')
}
android {
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
new file mode 100644
index 0000000..f87ab3d
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.privacysandbox.ui.client
+
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.os.Binder
+import android.os.Build
+import android.os.Bundle
+import android.view.Display
+import android.view.SurfaceControlViewHost
+import android.view.SurfaceView
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.privacysandbox.ui.core.IRemoteSessionClient
+import androidx.privacysandbox.ui.core.IRemoteSessionController
+import androidx.privacysandbox.ui.core.ISandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import java.util.concurrent.Executor
+
+/**
+ * Provides an adapter created from the supplied Bundle which acts as a proxy between the host app
+ * and Binder provided by the provider of content.
+ * @throws IllegalArgumentException if CoreLibInfo does not contain a Binder with the key
+ * uiAdapterBinder
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+object SandboxedUiAdapterFactory {
+ // Bundle key is a binary compatibility requirement
+ private const val UI_ADAPTER_BINDER = "uiAdapterBinder"
+ fun createFromCoreLibInfo(coreLibInfo: Bundle): SandboxedUiAdapter {
+ val uiAdapterBinder = requireNotNull(coreLibInfo.getBinder(UI_ADAPTER_BINDER)) {
+ "Invalid CoreLibInfo bundle, missing $UI_ADAPTER_BINDER."
+ }
+ val adapterInterface = ISandboxedUiAdapter.Stub.asInterface(
+ uiAdapterBinder
+ )
+ return RemoteAdapter(adapterInterface)
+ }
+
+ private class RemoteAdapter(private val adapterInterface: ISandboxedUiAdapter) :
+ SandboxedUiAdapter {
+
+ override fun openSession(
+ context: Context,
+ initialWidth: Int,
+ initialHeight: Int,
+ isZOrderOnTop: Boolean,
+ clientExecutor: Executor,
+ client: SandboxedUiAdapter.SessionClient
+ ) {
+ val mDisplayManager =
+ context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+ val displayId = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY).displayId
+
+ adapterInterface.openRemoteSession(
+ Binder(), // Host Token
+ displayId,
+ initialWidth,
+ initialHeight,
+ isZOrderOnTop,
+ RemoteSessionClient(context, client, clientExecutor)
+ )
+ }
+
+ class RemoteSessionClient(
+ val context: Context,
+ val client: SandboxedUiAdapter.SessionClient,
+ val clientExecutor: Executor
+ ) : IRemoteSessionClient.Stub() {
+
+ override fun onRemoteSessionOpened(
+ surfacePackage: SurfaceControlViewHost.SurfacePackage,
+ remoteSessionController: IRemoteSessionController,
+ isZOrderOnTop: Boolean
+ ) {
+ val surfaceView = SurfaceView(context)
+ surfaceView.setChildSurfacePackage(surfacePackage)
+ surfaceView.setZOrderOnTop(isZOrderOnTop)
+
+ clientExecutor.execute {
+ client.onSessionOpened(SessionImpl(surfaceView, remoteSessionController))
+ }
+ }
+
+ override fun onRemoteSessionError(errorString: String) {
+ clientExecutor.execute {
+ client.onSessionError(Throwable(errorString))
+ }
+ }
+ }
+
+ private class SessionImpl(
+ val surfaceView: SurfaceView,
+ val remoteSessionController: IRemoteSessionController
+ ) : SandboxedUiAdapter.Session {
+
+ override val view: View = surfaceView
+
+ override fun close() {
+ remoteSessionController.close()
+ }
+ }
+ }
+}
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-client-documentation.md b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/androidx-privacysandbox-ui-ui-client-documentation.md
similarity index 100%
rename from privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-client-documentation.md
rename to privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/androidx-privacysandbox-ui-ui-client-documentation.md
diff --git a/privacysandbox/ui/ui-core/api/current.txt b/privacysandbox/ui/ui-core/api/current.txt
index e6f50d0..3ab33d6 100644
--- a/privacysandbox/ui/ui-core/api/current.txt
+++ b/privacysandbox/ui/ui-core/api/current.txt
@@ -1 +1,27 @@
// Signature format: 4.0
+package androidx.privacysandbox.ui.core {
+
+ public interface SandboxedUiAdapter {
+ method public void openSession(android.content.Context context, int initialWidth, int initialHeight, boolean isZOrderOnTop, java.util.concurrent.Executor clientExecutor, androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient client);
+ }
+
+ public static interface SandboxedUiAdapter.Session {
+ method public void close();
+ method public android.view.View getView();
+ property public abstract android.view.View view;
+ }
+
+ public static interface SandboxedUiAdapter.SessionClient {
+ method public void onSessionError(Throwable throwable);
+ method public void onSessionOpened(androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session session);
+ }
+
+ public final class SdkRuntimeUiLibVersions {
+ method public int getClientVersion();
+ property public final int clientVersion;
+ field public static final androidx.privacysandbox.ui.core.SdkRuntimeUiLibVersions INSTANCE;
+ field public static final int apiVersion = 1; // 0x1
+ }
+
+}
+
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 e6f50d0..3ab33d6 100644
--- a/privacysandbox/ui/ui-core/api/public_plus_experimental_current.txt
+++ b/privacysandbox/ui/ui-core/api/public_plus_experimental_current.txt
@@ -1 +1,27 @@
// Signature format: 4.0
+package androidx.privacysandbox.ui.core {
+
+ public interface SandboxedUiAdapter {
+ method public void openSession(android.content.Context context, int initialWidth, int initialHeight, boolean isZOrderOnTop, java.util.concurrent.Executor clientExecutor, androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient client);
+ }
+
+ public static interface SandboxedUiAdapter.Session {
+ method public void close();
+ method public android.view.View getView();
+ property public abstract android.view.View view;
+ }
+
+ public static interface SandboxedUiAdapter.SessionClient {
+ method public void onSessionError(Throwable throwable);
+ method public void onSessionOpened(androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session session);
+ }
+
+ public final class SdkRuntimeUiLibVersions {
+ method public int getClientVersion();
+ property public final int clientVersion;
+ field public static final androidx.privacysandbox.ui.core.SdkRuntimeUiLibVersions INSTANCE;
+ field public static final int apiVersion = 1; // 0x1
+ }
+
+}
+
diff --git a/privacysandbox/ui/ui-core/api/restricted_current.txt b/privacysandbox/ui/ui-core/api/restricted_current.txt
index e6f50d0..3ab33d6 100644
--- a/privacysandbox/ui/ui-core/api/restricted_current.txt
+++ b/privacysandbox/ui/ui-core/api/restricted_current.txt
@@ -1 +1,27 @@
// Signature format: 4.0
+package androidx.privacysandbox.ui.core {
+
+ public interface SandboxedUiAdapter {
+ method public void openSession(android.content.Context context, int initialWidth, int initialHeight, boolean isZOrderOnTop, java.util.concurrent.Executor clientExecutor, androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient client);
+ }
+
+ public static interface SandboxedUiAdapter.Session {
+ method public void close();
+ method public android.view.View getView();
+ property public abstract android.view.View view;
+ }
+
+ public static interface SandboxedUiAdapter.SessionClient {
+ method public void onSessionError(Throwable throwable);
+ method public void onSessionOpened(androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session session);
+ }
+
+ public final class SdkRuntimeUiLibVersions {
+ method public int getClientVersion();
+ property public final int clientVersion;
+ field public static final androidx.privacysandbox.ui.core.SdkRuntimeUiLibVersions INSTANCE;
+ field public static final int apiVersion = 1; // 0x1
+ }
+
+}
+
diff --git a/privacysandbox/ui/ui-core/build.gradle b/privacysandbox/ui/ui-core/build.gradle
index 4e0e9bd..f0b88cd 100644
--- a/privacysandbox/ui/ui-core/build.gradle
+++ b/privacysandbox/ui/ui-core/build.gradle
@@ -24,11 +24,21 @@
dependencies {
api(libs.kotlinStdlib)
- // Add dependencies here
+ implementation 'androidx.annotation:annotation:1.5.0'
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.kotlinStdlib)
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.truth)
}
android {
namespace "androidx.privacysandbox.ui.core"
+ buildFeatures {
+ aidl = true
+ }
}
androidx {
diff --git a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionClient.aidl b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionClient.aidl
new file mode 100644
index 0000000..63a66c1
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionClient.aidl
@@ -0,0 +1,27 @@
+/*
+ * 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.privacysandbox.ui.core;
+
+import androidx.privacysandbox.ui.core.IRemoteSessionController;
+import android.view.SurfaceControlViewHost.SurfacePackage;
+
+/** @hide */
+oneway interface IRemoteSessionClient {
+ void onRemoteSessionOpened(in SurfacePackage surfacePackage,
+ IRemoteSessionController remoteSessionController, boolean isZOrderOnTop);
+ void onRemoteSessionError(String exception);
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl
new file mode 100644
index 0000000..b64a992
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl
@@ -0,0 +1,22 @@
+/*
+ * 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.privacysandbox.ui.core;
+
+/** @hide */
+oneway interface IRemoteSessionController {
+ void close();
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/ISandboxedUiAdapter.aidl b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/ISandboxedUiAdapter.aidl
new file mode 100644
index 0000000..a452da6
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/ISandboxedUiAdapter.aidl
@@ -0,0 +1,27 @@
+/*
+ * 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.privacysandbox.ui.core;
+
+import androidx.privacysandbox.ui.core.IRemoteSessionClient;
+import android.content.Context;
+
+/** @hide */
+oneway interface ISandboxedUiAdapter {
+ void openRemoteSession(
+ IBinder hostToken, int displayId, int initialWidth, int initialHeight, boolean isZOrderOnTop,
+ IRemoteSessionClient remoteSessionClient);
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt
new file mode 100644
index 0000000..c6751e3
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedUiAdapter.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.privacysandbox.ui.core
+
+import android.content.Context
+import android.view.View
+import java.util.concurrent.Executor
+
+/**
+ * An Adapter that provides content from a SandboxedSdk to be displayed as part of a host app's UI.
+ */
+
+interface SandboxedUiAdapter {
+
+ /**
+ * Open a new session for displaying content with an initial size of
+ * [initialWidth]x[initialHeight] pixels. [client] will receive all incoming communication from
+ * the provider of content. All incoming calls to [client] will be made through the provided
+ * [clientExecutor]. [isZOrderOnTop] tracks if the content surface will be placed on top of its
+ * window
+ */
+ fun openSession(
+ context: Context,
+ initialWidth: Int,
+ initialHeight: Int,
+ isZOrderOnTop: Boolean,
+ clientExecutor: Executor,
+ client: SessionClient
+ )
+
+ /**
+ * A single session with the provider of remote content.
+ */
+ interface Session {
+
+ /**
+ * Return the [View] that presents content for this session. The same view will be returned
+ * for the life of the session object. Accessing [view] after [close] may throw an
+ * [IllegalStateException].
+ */
+ val view: View
+
+ /**
+ * Close this session, indicating that the remote provider of content should
+ * dispose of associated resources and that the [SessionClient] should not
+ * receive further callback events.
+ */
+ fun close()
+ }
+
+ /**
+ * The client of a single session that will receive callback events from an active session.
+ */
+ interface SessionClient {
+ /**
+ * Called to report that the session was opened successfully, delivering the [Session]
+ * handle that should be used to notify the session of UI events.
+ */
+ fun onSessionOpened(session: Session)
+
+ /**
+ * Called to report a terminal error in the session. No further events will be reported
+ * to this [SessionClient] and any further or currently pending calls to the [Session]
+ * that may have been in flight may be ignored.
+ */
+ fun onSessionError(throwable: Throwable)
+ }
+}
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SdkRuntimeUiLibVersions.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SdkRuntimeUiLibVersions.kt
new file mode 100644
index 0000000..ce0f622
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SdkRuntimeUiLibVersions.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.privacysandbox.ui.core
+
+import androidx.annotation.RestrictTo
+
+object SdkRuntimeUiLibVersions {
+ var clientVersion: Int = -1
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ set
+
+ const val apiVersion: Int = 1
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-core-documentation.md b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/androidx-privacysandbox-ui-ui-core-documentation.md
similarity index 99%
rename from privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-core-documentation.md
rename to privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/androidx-privacysandbox-ui-ui-core-documentation.md
index aa56a66..df34ca3 100644
--- a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-core-documentation.md
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/androidx-privacysandbox-ui-ui-core-documentation.md
@@ -3,5 +3,6 @@
Privacy Sandbox Ui Core
# Package androidx.privacysandbox.ui.core
+
This package contains interface and class definitions shared between Privacy
Sandbox Ui Client and Privacy Sandbox Ui Provider.
diff --git a/privacysandbox/ui/ui-provider/api/current.txt b/privacysandbox/ui/ui-provider/api/current.txt
index e6f50d0..20170b4 100644
--- a/privacysandbox/ui/ui-provider/api/current.txt
+++ b/privacysandbox/ui/ui-provider/api/current.txt
@@ -1 +1,9 @@
// Signature format: 4.0
+package androidx.privacysandbox.ui.provider {
+
+ @RequiresApi(33) public final class SandboxedUiAdapterProxy {
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static android.os.Bundle toCoreLibInfo(androidx.privacysandbox.ui.core.SandboxedUiAdapter, android.content.Context context);
+ }
+
+}
+
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 e6f50d0..20170b4 100644
--- a/privacysandbox/ui/ui-provider/api/public_plus_experimental_current.txt
+++ b/privacysandbox/ui/ui-provider/api/public_plus_experimental_current.txt
@@ -1 +1,9 @@
// Signature format: 4.0
+package androidx.privacysandbox.ui.provider {
+
+ @RequiresApi(33) public final class SandboxedUiAdapterProxy {
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static android.os.Bundle toCoreLibInfo(androidx.privacysandbox.ui.core.SandboxedUiAdapter, android.content.Context context);
+ }
+
+}
+
diff --git a/privacysandbox/ui/ui-provider/api/restricted_current.txt b/privacysandbox/ui/ui-provider/api/restricted_current.txt
index e6f50d0..20170b4 100644
--- a/privacysandbox/ui/ui-provider/api/restricted_current.txt
+++ b/privacysandbox/ui/ui-provider/api/restricted_current.txt
@@ -1 +1,9 @@
// Signature format: 4.0
+package androidx.privacysandbox.ui.provider {
+
+ @RequiresApi(33) public final class SandboxedUiAdapterProxy {
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static android.os.Bundle toCoreLibInfo(androidx.privacysandbox.ui.core.SandboxedUiAdapter, android.content.Context context);
+ }
+
+}
+
diff --git a/privacysandbox/ui/ui-provider/build.gradle b/privacysandbox/ui/ui-provider/build.gradle
index 6b2b1324..bcbd719 100644
--- a/privacysandbox/ui/ui-provider/build.gradle
+++ b/privacysandbox/ui/ui-provider/build.gradle
@@ -24,7 +24,15 @@
dependencies {
api(libs.kotlinStdlib)
- // Add dependencies here
+ implementation project(path: ':privacysandbox:ui:ui-core')
+ implementation project(path: ':annotation:annotation')
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.kotlinStdlib)
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.truth)
}
android {
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
new file mode 100644
index 0000000..84149c0
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
@@ -0,0 +1,138 @@
+/*
+ * 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(Build.VERSION_CODES.TIRAMISU)
+@file:JvmName("SandboxedUiAdapterProxy")
+
+package androidx.privacysandbox.ui.provider
+
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.IBinder
+import android.os.Looper
+import android.view.SurfaceControlViewHost
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.privacysandbox.ui.core.IRemoteSessionClient
+import androidx.privacysandbox.ui.core.IRemoteSessionController
+import androidx.privacysandbox.ui.core.ISandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import java.util.concurrent.Executor
+
+/**
+ * Provides a [Bundle] containing a Binder which represents a [SandboxedUiAdapter]. The Bundle
+ * is shuttled to the host app in order for the [SandboxedUiAdapter] to be used to retrieve
+ * content.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+fun SandboxedUiAdapter.toCoreLibInfo(@Suppress("ContextFirst") context: Context): Bundle {
+ val binderAdapter = BinderAdapterDelegate(context, this)
+ // TODO: Add version info
+ val bundle = Bundle()
+ // Bundle key is a binary compatibility requirement
+ bundle.putBinder("uiAdapterBinder", binderAdapter)
+ return bundle
+}
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+private class BinderAdapterDelegate(
+ private val sandboxContext: Context,
+ private val adapter: SandboxedUiAdapter
+) : ISandboxedUiAdapter.Stub() {
+
+ fun openSession(
+ context: Context,
+ initialWidth: Int,
+ initialHeight: Int,
+ isZOrderOnTop: Boolean,
+ clientExecutor: Executor,
+ client: SandboxedUiAdapter.SessionClient
+ ) {
+ adapter.openSession(
+ context, initialWidth, initialHeight, isZOrderOnTop, clientExecutor,
+ client
+ )
+ }
+
+ override fun openRemoteSession(
+ hostToken: IBinder,
+ displayId: Int,
+ initialWidth: Int,
+ initialHeight: Int,
+ isZOrderOnTop: Boolean,
+ remoteSessionClient: IRemoteSessionClient
+ ) {
+ val mHandler = Handler(Looper.getMainLooper())
+ mHandler.post {
+ try {
+ val mDisplayManager: DisplayManager =
+ sandboxContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+ val windowContext =
+ sandboxContext.createDisplayContext(mDisplayManager.getDisplay(displayId))
+ val surfaceControlViewHost = SurfaceControlViewHost(
+ windowContext,
+ mDisplayManager.getDisplay(displayId), hostToken
+ )
+ val sessionClient = SessionClientProxy(
+ surfaceControlViewHost, initialWidth, initialHeight, remoteSessionClient
+ )
+ openSession(
+ windowContext, initialWidth, initialHeight, isZOrderOnTop,
+ Runnable::run, sessionClient
+ )
+ } catch (exception: Throwable) {
+ remoteSessionClient.onRemoteSessionError(exception.message)
+ }
+ }
+ }
+
+ private inner class SessionClientProxy(
+ private val surfaceControlViewHost: SurfaceControlViewHost,
+ private val initialWidth: Int,
+ private val initialHeight: Int,
+ private val remoteSessionClient: IRemoteSessionClient
+ ) : SandboxedUiAdapter.SessionClient {
+
+ override fun onSessionOpened(session: SandboxedUiAdapter.Session) {
+ val view = session.view
+ surfaceControlViewHost.setView(view, initialWidth, initialHeight)
+ val surfacePackage = surfaceControlViewHost.surfacePackage
+ val remoteSessionController =
+ RemoteSessionController(surfaceControlViewHost, session)
+ remoteSessionClient.onRemoteSessionOpened(
+ surfacePackage, remoteSessionController,
+ /* isZOrderOnTop= */ true
+ )
+ }
+
+ override fun onSessionError(throwable: Throwable) {
+ remoteSessionClient.onRemoteSessionError(throwable.message)
+ }
+
+ @VisibleForTesting
+ private inner class RemoteSessionController(
+ val surfaceControlViewHost: SurfaceControlViewHost,
+ val session: SandboxedUiAdapter.Session
+ ) : IRemoteSessionController.Stub() {
+ override fun close() {
+ session.close()
+ surfaceControlViewHost.release()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-provider-documentation.md b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/androidx-privacysandbox-ui-ui-provider-documentation.md
similarity index 100%
rename from privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-provider-documentation.md
rename to privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/androidx-privacysandbox-ui-ui-provider-documentation.md
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
index b0b500b..29f861c7 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
@@ -134,8 +134,14 @@
fun getBook(bookId: String): Book
@Query("SELECT * FROM book WHERE bookId = :bookId")
+ fun getBookNullable(bookId: String): Book?
+
+ @Query("SELECT * FROM book WHERE bookId = :bookId")
suspend fun getBookSuspend(bookId: String): Book
+ @Query("SELECT * FROM book WHERE bookId = :bookId")
+ suspend fun getBookNullableSuspend(bookId: String): Book?
+
@Query("SELECT * FROM book")
suspend fun getBooksSuspend(): List<Book>
@@ -366,6 +372,9 @@
@Query("SELECT * FROM Publisher WHERE publisherId = :publisherId")
fun getPublisher(publisherId: String): Publisher
+ @Query("SELECT * FROM Publisher WHERE publisherId = :publisherId")
+ fun getPublisherNullable(publisherId: String): Publisher?
+
@Query("SELECT * FROM Publisher WHERE _rowid_ = :rowid")
fun getPublisher(rowid: Long): Publisher
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BooksDaoTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BooksDaoTest.kt
index 49b2b5f..370e1f7 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BooksDaoTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BooksDaoTest.kt
@@ -30,11 +30,12 @@
import io.reactivex.Flowable
import io.reactivex.schedulers.Schedulers
import io.reactivex.subscribers.TestSubscriber
+import java.util.Date
import kotlinx.coroutines.runBlocking
import org.hamcrest.CoreMatchers
-import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.instanceOf
+import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.notNullValue
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Assert.assertEquals
@@ -42,7 +43,6 @@
import org.junit.Assert.assertNull
import org.junit.Assert.fail
import org.junit.Test
-import java.util.Date
@MediumTest
class BooksDaoTest : TestDatabaseTest() {
@@ -431,7 +431,7 @@
@Test
fun kotlinDefaultFunction() {
booksDao.addAndRemovePublisher(TestUtil.PUBLISHER)
- assertNull(booksDao.getPublisher(TestUtil.PUBLISHER.publisherId))
+ assertNull(booksDao.getPublisherNullable(TestUtil.PUBLISHER.publisherId))
assertEquals("", booksDao.concreteFunction())
assertEquals("1 - hello", booksDao.concreteFunctionWithParams(1, "hello"))
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DeferredBooksDaoTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DeferredBooksDaoTest.kt
index 1ad5028..24ddfac 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DeferredBooksDaoTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DeferredBooksDaoTest.kt
@@ -142,14 +142,14 @@
@Test
fun deleteBookWithIds() {
booksDao.deleteBookWithIds(TestUtil.BOOK_1.bookId)
- assertThat(booksDao.getBook(TestUtil.BOOK_1.bookId), nullValue())
+ assertThat(booksDao.getBookNullable(TestUtil.BOOK_1.bookId), nullValue())
}
@Test
fun deleteBookWithIdsSuspend() {
runBlocking {
booksDao.deleteBookWithIdsSuspend(TestUtil.BOOK_1.bookId)
- assertThat(booksDao.getBookSuspend(TestUtil.BOOK_1.bookId), nullValue())
+ assertThat(booksDao.getBookNullableSuspend(TestUtil.BOOK_1.bookId), nullValue())
}
}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DependencyDaoTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DependencyDaoTest.kt
index 7e9e88b..f5bd5fd 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DependencyDaoTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DependencyDaoTest.kt
@@ -92,19 +92,19 @@
)
dao.insert(foo1, foo2, bar)
val fooList = dao.relation("foo")
- assertThat(fooList.sharedName, `is`("foo"))
assertThat(fooList, `is`(notNullValue()))
- assertThat(fooList.dataItems, `is`(listOf(foo1, foo2)))
+ assertThat(fooList?.sharedName, `is`("foo"))
+ assertThat(fooList?.dataItems, `is`(listOf(foo1, foo2)))
val barList = dao.relation("bar")
- assertThat(barList.sharedName, `is`("bar"))
assertThat(barList, `is`(notNullValue()))
- assertThat(barList.dataItems, `is`(listOf(bar)))
+ assertThat(barList?.sharedName, `is`("bar"))
+ assertThat(barList?.dataItems, `is`(listOf(bar)))
val bazList = dao.relation("baz")
- assertThat(bazList.sharedName, `is`("baz"))
assertThat(bazList, `is`(notNullValue()))
- assertThat(bazList.dataItems, `is`(emptyList()))
+ assertThat(bazList?.sharedName, `is`("baz"))
+ assertThat(bazList?.dataItems, `is`(emptyList()))
}
private fun insertSample(id: Int): DataClassFromDependency {
diff --git a/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/dao/DependencyDao.kt b/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/dao/DependencyDao.kt
index 5365d47..487f747 100644
--- a/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/dao/DependencyDao.kt
+++ b/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/dao/DependencyDao.kt
@@ -33,18 +33,18 @@
fun selectAll(): List<DataClassFromDependency>
@Query("select * from DataClassFromDependency where id = :id LIMIT 1")
- fun findEmbedded(id: Int): EmbeddedFromDependency
+ fun findEmbedded(id: Int): EmbeddedFromDependency?
@Query("select * from DataClassFromDependency where id = :id LIMIT 1")
- fun findPojo(id: Int): PojoFromDependency
+ fun findPojo(id: Int): PojoFromDependency?
@Query("select * from DataClassFromDependency where id = :id LIMIT 1")
- fun findById(id: Int): DataClassFromDependency
+ fun findById(id: Int): DataClassFromDependency?
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@Transaction
@Query("WITH nameTable( sharedName ) AS ( SELECT :name ) SELECT * from nameTable")
- fun relation(name: String): RelationFromDependency
+ fun relation(name: String): RelationFromDependency?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg input: DataClassFromDependency)
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
index 6d87e42..7b781ec 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
@@ -191,7 +191,9 @@
addModifiers(Modifier.PROTECTED)
}
addAnnotation(Override::class.java)
- varargs(executableElement.isVarArgs())
+ // In Java, only the last argument can be a vararg so for suspend functions, it is never
+ // a vararg function.
+ varargs(!executableElement.isSuspendFunction() && executableElement.isVarArgs())
executableElement.thrownTypes.forEach {
addException(it.asTypeName().java)
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/KotlinPoetExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/KotlinPoetExt.kt
index 87ab8c2..d8258a7 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/KotlinPoetExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/KotlinPoetExt.kt
@@ -76,16 +76,17 @@
}
// TODO(b/251316420): Add type variable names
val isVarArgs = executableElement.isVarArgs()
- resolvedType.parameterTypes.let {
+ val parameterTypes = resolvedType.parameterTypes.let {
// Drop the synthetic Continuation param of suspend functions, always at the last
// position.
// TODO(b/254135327): Revisit with the introduction of a target language.
if (resolvedType.isSuspendFunction()) it.dropLast(1) else it
- }.forEachIndexed { index, paramType ->
+ }
+ parameterTypes.forEachIndexed { index, paramType ->
val typeName: XTypeName
val modifiers: Array<KModifier>
// TODO(b/253268357): In Kotlin the vararg is not always the last param
- if (isVarArgs && index == resolvedType.parameterTypes.size - 1) {
+ if (isVarArgs && index == parameterTypes.size - 1) {
typeName = (paramType as XArrayType).componentType.asTypeName()
modifiers = arrayOf(KModifier.VARARG)
} else {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
index 818856d..9e74ffb 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
@@ -27,7 +27,6 @@
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.isConstructor
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
-import com.google.devtools.ksp.symbol.Modifier
internal abstract class KspExecutableElement(
env: KspProcessingEnv,
@@ -63,12 +62,7 @@
}
override fun isVarArgs(): Boolean {
- // in java, only the last argument can be a vararg so for suspend functions, it is never
- // a vararg function. this would change if room generated kotlin code
- return !declaration.modifiers.contains(Modifier.SUSPEND) &&
- declaration.parameters.any {
- it.isVararg
- }
+ return declaration.parameters.any { it.isVararg }
}
companion object {
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
index 705d9f0..73c87b1 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
@@ -135,7 +135,11 @@
) {
val element = it.processingEnv.requireTypeElement("Subject")
assertThat(element.getMethodByJvmName("method").isVarArgs()).isTrue()
- assertThat(element.getMethodByJvmName("suspendMethod").isVarArgs()).isFalse()
+ if (it.isKsp) {
+ assertThat(element.getMethodByJvmName("suspendMethod").isVarArgs()).isTrue()
+ } else {
+ assertThat(element.getMethodByJvmName("suspendMethod").isVarArgs()).isFalse()
+ }
}
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/package_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/package_ext.kt
index 446eb00..515bda9 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/package_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/package_ext.kt
@@ -26,7 +26,7 @@
*
* Example of a typical config:
* ```
- * # Room dejetifier packages for JavaPoet class names.
+ * # Room dejetifier packages for XPoet class names.
* androidx.sqlite = android.arch.persistence
* androidx.room = android.arch.persistence.room
* androidx.paging = android.arch.paging
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
index b6158fa..8895edf 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
@@ -28,15 +28,8 @@
import androidx.room.compiler.codegen.XTypeSpec
import androidx.room.compiler.codegen.asClassName
import androidx.room.compiler.codegen.asMutableClassName
-import com.squareup.javapoet.ArrayTypeName
-import com.squareup.javapoet.ClassName
+import com.squareup.kotlinpoet.javapoet.JTypeName
import java.util.concurrent.Callable
-import kotlin.reflect.KClass
-
-val KClass<*>.typeName: ClassName
- get() = ClassName.get(this.java)
-val KClass<*>.arrayTypeName: ArrayTypeName
- get() = ArrayTypeName.of(typeName)
object SupportDbTypeNames {
val DB = XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteDatabase")
@@ -83,10 +76,8 @@
}
object PagingTypeNames {
- val DATA_SOURCE: ClassName =
- ClassName.get(PAGING_PACKAGE, "DataSource")
- val POSITIONAL_DATA_SOURCE: ClassName =
- ClassName.get(PAGING_PACKAGE, "PositionalDataSource")
+ val DATA_SOURCE = XClassName.get(PAGING_PACKAGE, "DataSource")
+ val POSITIONAL_DATA_SOURCE = XClassName.get(PAGING_PACKAGE, "PositionalDataSource")
val DATA_SOURCE_FACTORY = XClassName.get(PAGING_PACKAGE, "DataSource", "Factory")
val PAGING_SOURCE = XClassName.get(PAGING_PACKAGE, "PagingSource")
val LISTENABLE_FUTURE_PAGING_SOURCE =
@@ -326,7 +317,7 @@
callBody()
}.apply(
javaMethodBuilder = {
- addException(Exception::class.typeName)
+ addException(JTypeName.get(Exception::class.java))
},
kotlinFunctionBuilder = { }
).build()
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/TransactionMethodProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/TransactionMethodProcessor.kt
index 24add23..71f283c 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/TransactionMethodProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/TransactionMethodProcessor.kt
@@ -16,6 +16,7 @@
package androidx.room.processor
+import androidx.room.compiler.codegen.CodeLanguage
import androidx.room.compiler.processing.XMethodElement
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeElement
@@ -68,10 +69,22 @@
TransactionMethod.CallType.CONCRETE
}
+ val isVarArgs = executableElement.isVarArgs()
+ val parameters = delegate.extractParams()
+ val processedParamNames = parameters.mapIndexed { index, param ->
+ // Apply spread operator when delegating to a vararg parameter in Kotlin.
+ if (context.codeLanguage == CodeLanguage.KOTLIN &&
+ isVarArgs && index == parameters.size - 1) {
+ "*${param.name}"
+ } else {
+ param.name
+ }
+ }
+
return TransactionMethod(
element = executableElement,
returnType = returnType,
- parameterNames = delegate.extractParams().map { it.name },
+ parameterNames = processedParamNames,
callType = callType,
methodBinder = delegate.findTransactionMethodBinder(callType)
)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt
index 9d622a4..a4e230b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt
@@ -17,9 +17,7 @@
package androidx.room.solver
import androidx.annotation.VisibleForTesting
-import androidx.room.compiler.codegen.JCodeBlockBuilder
import androidx.room.compiler.codegen.XCodeBlock
-import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.apply
import androidx.room.writer.TypeWriter
/**
@@ -44,17 +42,6 @@
"$prefix${if (index == 0) "" else "_$index"}"
}
- // TODO(b/248383583): Remove once XPoet is more widely adopted.
- // @Deprecated("Use property, will be removed once more writers are migrated to XPoet")
- fun builder(): JCodeBlockBuilder {
- lateinit var leakJavaBuilder: JCodeBlockBuilder
- builder.apply(
- javaCodeBuilder = { leakJavaBuilder = this },
- kotlinCodeBuilder = { error("KotlinPoet code builder should have not been invoked!") }
- )
- return leakJavaBuilder
- }
-
fun getTmpVar(): String {
return getTmpVar(TMP_VAR_DEFAULT_PREFIX)
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index ffd31b6..8d6dbe0 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -18,6 +18,7 @@
import androidx.annotation.VisibleForTesting
import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XTypeName
import androidx.room.compiler.processing.XNullability
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.isArray
@@ -124,9 +125,7 @@
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableMultimap
import com.google.common.collect.ImmutableSetMultimap
-import com.squareup.javapoet.TypeName
-@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
/**
* Holds all type adapters and can create on demand composite type adapters to convert a type into a
* database column.
@@ -202,7 +201,7 @@
}
}
- val queryResultBinderProviders: List<QueryResultBinderProvider> =
+ private val queryResultBinderProviders: List<QueryResultBinderProvider> =
mutableListOf<QueryResultBinderProvider>().apply {
add(CursorQueryResultBinderProvider(context))
add(LiveDataQueryResultBinderProvider(context))
@@ -219,28 +218,28 @@
add(InstantQueryResultBinderProvider(context))
}
- val preparedQueryResultBinderProviders: List<PreparedQueryResultBinderProvider> =
+ private val preparedQueryResultBinderProviders: List<PreparedQueryResultBinderProvider> =
mutableListOf<PreparedQueryResultBinderProvider>().apply {
addAll(RxPreparedQueryResultBinderProvider.getAll(context))
add(GuavaListenableFuturePreparedQueryResultBinderProvider(context))
add(InstantPreparedQueryResultBinderProvider(context))
}
- val insertBinderProviders: List<InsertOrUpsertMethodBinderProvider> =
+ private val insertBinderProviders: List<InsertOrUpsertMethodBinderProvider> =
mutableListOf<InsertOrUpsertMethodBinderProvider>().apply {
addAll(RxCallableInsertMethodBinderProvider.getAll(context))
add(GuavaListenableFutureInsertMethodBinderProvider(context))
add(InstantInsertMethodBinderProvider(context))
}
- val deleteOrUpdateBinderProvider: List<DeleteOrUpdateMethodBinderProvider> =
+ private val deleteOrUpdateBinderProvider: List<DeleteOrUpdateMethodBinderProvider> =
mutableListOf<DeleteOrUpdateMethodBinderProvider>().apply {
addAll(RxCallableDeleteOrUpdateMethodBinderProvider.getAll(context))
add(GuavaListenableFutureDeleteOrUpdateMethodBinderProvider(context))
add(InstantDeleteOrUpdateMethodBinderProvider(context))
}
- val upsertBinderProviders: List<InsertOrUpsertMethodBinderProvider> =
+ private val upsertBinderProviders: List<InsertOrUpsertMethodBinderProvider> =
mutableListOf<InsertOrUpsertMethodBinderProvider>().apply {
addAll(RxCallableUpsertMethodBinderProvider.getAll(context))
add(GuavaListenableFutureUpsertMethodBinderProvider(context))
@@ -624,9 +623,9 @@
}
val keyTypeArg = when (mapType) {
MultimapQueryResultAdapter.MapType.LONG_SPARSE ->
- context.processingEnv.requireType(TypeName.LONG)
+ context.processingEnv.requireType(XTypeName.PRIMITIVE_LONG)
MultimapQueryResultAdapter.MapType.INT_SPARSE ->
- context.processingEnv.requireType(TypeName.INT)
+ context.processingEnv.requireType(XTypeName.PRIMITIVE_INT)
else ->
typeMirror.typeArguments[0].extendsBoundOrSelf()
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt
index a3c2db3f..25688ea 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt
@@ -30,11 +30,15 @@
class DataSourceQueryResultBinderProvider(val context: Context) : QueryResultBinderProvider {
private val dataSourceType: XRawType? by lazy {
- context.processingEnv.findType(PagingTypeNames.DATA_SOURCE)?.rawType
+ context.processingEnv.findType(
+ PagingTypeNames.DATA_SOURCE.canonicalName
+ )?.rawType
}
private val positionalDataSourceType: XRawType? by lazy {
- context.processingEnv.findType(PagingTypeNames.POSITIONAL_DATA_SOURCE)?.rawType
+ context.processingEnv.findType(
+ PagingTypeNames.POSITIONAL_DATA_SOURCE.canonicalName
+ )?.rawType
}
override fun provide(
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt
index 093dcc2..170837e 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt
@@ -17,13 +17,13 @@
package androidx.room.processor
import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XTypeName
import androidx.room.parser.SQLTypeAffinity
import androidx.room.vo.CallType
import androidx.room.vo.Field
import androidx.room.vo.FieldGetter
import androidx.room.vo.FieldSetter
import com.google.common.truth.Truth.assertThat
-import com.squareup.javapoet.TypeName
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@@ -65,7 +65,7 @@
.isEqualTo("foo.bar.MyEntity")
assertThat(entity.fields.size).isEqualTo(1)
val field = entity.fields.first()
- val intType = invocation.processingEnv.requireType(TypeName.INT)
+ val intType = invocation.processingEnv.requireType(XTypeName.PRIMITIVE_INT)
assertThat(field).isEqualTo(
Field(
element = field.element,
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts3TableEntityProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts3TableEntityProcessorTest.kt
index 5fe3711..ec64f94 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts3TableEntityProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts3TableEntityProcessorTest.kt
@@ -34,6 +34,7 @@
import androidx.room.FtsOptions
import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XTypeName
import androidx.room.parser.FtsVersion
import androidx.room.parser.SQLTypeAffinity
import androidx.room.vo.CallType
@@ -41,7 +42,6 @@
import androidx.room.vo.FieldGetter
import androidx.room.vo.FieldSetter
import androidx.room.vo.Fields
-import com.squareup.javapoet.TypeName
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
@@ -68,7 +68,7 @@
`is`("foo.bar.MyEntity"))
assertThat(entity.fields.size, `is`(1))
val field = entity.fields.first()
- val intType = invocation.processingEnv.requireType(TypeName.INT)
+ val intType = invocation.processingEnv.requireType(XTypeName.PRIMITIVE_INT)
assertThat(
field,
`is`(
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
index 9b1f407..fef806c 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
@@ -18,6 +18,7 @@
import androidx.room.FtsOptions
import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XTypeName
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.parser.FtsVersion
@@ -28,7 +29,6 @@
import androidx.room.vo.FieldGetter
import androidx.room.vo.FieldSetter
import androidx.room.vo.Fields
-import com.squareup.javapoet.TypeName
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
@@ -55,7 +55,7 @@
`is`("foo.bar.MyEntity"))
assertThat(entity.fields.size, `is`(1))
val field = entity.fields.first()
- val intType = invocation.processingEnv.requireType(TypeName.INT)
+ val intType = invocation.processingEnv.requireType(XTypeName.PRIMITIVE_INT)
assertThat(
field,
`is`(
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTargetMethodTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTargetMethodTest.kt
index d867db1..0a35d47 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTargetMethodTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTargetMethodTest.kt
@@ -16,11 +16,11 @@
package androidx.room.processor
+import androidx.room.compiler.codegen.XClassName
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.XTestInvocation
import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.testing.context
-import com.squareup.javapoet.ClassName
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -29,8 +29,8 @@
class PojoProcessorTargetMethodTest {
companion object {
- val MY_POJO: ClassName = ClassName.get("foo.bar", "MyPojo")
- val AUTOVALUE_MY_POJO: ClassName = ClassName.get("foo.bar", "AutoValue_MyPojo")
+ val MY_POJO = XClassName.get("foo.bar", "MyPojo")
+ val AUTOVALUE_MY_POJO = XClassName.get("foo.bar", "AutoValue_MyPojo")
const val HEADER = """
package foo.bar;
@@ -56,7 +56,7 @@
@Test
fun invalidAnnotationInMethod() {
val source = Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
@@ -83,7 +83,7 @@
@Test
fun invalidAnnotationInStaticMethod() {
val source = Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
@@ -110,7 +110,7 @@
@Test
fun invalidAnnotationInAbstractMethod() {
val source = Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
@@ -222,7 +222,7 @@
@Test
fun validAnnotationInField() {
val source = Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
@@ -240,7 +240,7 @@
@Test
fun validAnnotationInStaticField() {
val source = Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
@@ -493,8 +493,8 @@
vararg sources: Source,
handler: ((XTestInvocation) -> Unit)? = null
) {
- val pojoSource = Source.java(MY_POJO.toString(), pojoCode)
- val autoValuePojoSource = Source.java(AUTOVALUE_MY_POJO.toString(), autoValuePojoCode)
+ val pojoSource = Source.java(MY_POJO.canonicalName, pojoCode)
+ val autoValuePojoSource = Source.java(AUTOVALUE_MY_POJO.canonicalName, autoValuePojoCode)
val all = sources.toList() + pojoSource + autoValuePojoSource
return runProcessorTest(
sources = all
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
index c93ac24..7623237 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
@@ -42,7 +42,6 @@
import androidx.room.vo.Pojo
import androidx.room.vo.RelationCollector
import com.google.common.truth.Truth
-import com.squareup.javapoet.ClassName
import java.io.File
import org.hamcrest.CoreMatchers.instanceOf
import org.hamcrest.CoreMatchers.`is`
@@ -64,7 +63,7 @@
class PojoProcessorTest {
companion object {
- val MY_POJO: ClassName = ClassName.get("foo.bar", "MyPojo")
+ val MY_POJO = XClassName.get("foo.bar", "MyPojo")
val HEADER = """
package foo.bar;
import androidx.room.*;
@@ -88,11 +87,11 @@
runProcessorTest(
sources = listOf(
Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
import androidx.room.*;
- public class ${MY_POJO.simpleName()} extends foo.bar.x.BaseClass {
+ public class ${MY_POJO.simpleNames.single()} extends foo.bar.x.BaseClass {
public String myField;
}
"""
@@ -1060,7 +1059,7 @@
@Test
fun cache() {
val pojo = Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
$HEADER
int id;
@@ -1193,7 +1192,7 @@
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.ambiguousConstructor(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"name", listOf("mName", "_name")
)
)
@@ -1721,12 +1720,12 @@
@Test
fun ignoredColumns() {
val source = Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
import androidx.room.*;
@Entity(ignoredColumns = {"bar"})
- public class ${MY_POJO.simpleName()} {
+ public class ${MY_POJO.simpleNames.single()} {
public String foo;
public String bar;
}
@@ -1751,15 +1750,15 @@
runProcessorTest(
listOf(
Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
import androidx.room.*;
@Entity(ignoredColumns = {"bar"})
- public class ${MY_POJO.simpleName()} {
+ public class ${MY_POJO.simpleNames.single()} {
private final String foo;
private final String bar;
- public ${MY_POJO.simpleName()}(String foo) {
+ public ${MY_POJO.simpleNames.single()}(String foo) {
this.foo = foo;
this.bar = null;
}
@@ -1788,12 +1787,12 @@
runProcessorTest(
listOf(
Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
import androidx.room.*;
@Entity(ignoredColumns = {"bar"})
- public class ${MY_POJO.simpleName()} {
+ public class ${MY_POJO.simpleNames.single()} {
private String foo;
private String bar;
public String getFoo() {
@@ -1823,12 +1822,12 @@
runProcessorTest(
listOf(
Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
import androidx.room.*;
@Entity(ignoredColumns = {"my_bar"})
- public class ${MY_POJO.simpleName()} {
+ public class ${MY_POJO.simpleNames.single()} {
public String foo;
@ColumnInfo(name = "my_bar")
public String bar;
@@ -1853,12 +1852,12 @@
runProcessorTest(
listOf(
Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
import androidx.room.*;
@Entity(ignoredColumns = {"no_such_column"})
- public class ${MY_POJO.simpleName()} {
+ public class ${MY_POJO.simpleNames.single()} {
public String foo;
public String bar;
}
@@ -1887,11 +1886,11 @@
runProcessorTest(
listOf(
Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
import androidx.room.*;
- public class ${MY_POJO.simpleName()} {
+ public class ${MY_POJO.simpleNames.single()} {
private String foo;
private String bar;
public String getFoo() { return foo; }
@@ -1915,11 +1914,11 @@
runProcessorTest(
listOf(
Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
import androidx.room.*;
- public class ${MY_POJO.simpleName()} {
+ public class ${MY_POJO.simpleNames.single()} {
private String foo;
private String bar;
public String getFoo() { return foo; }
@@ -1948,11 +1947,11 @@
runProcessorTest(
listOf(
Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
import androidx.room.*;
- public class ${MY_POJO.simpleName()} {
+ public class ${MY_POJO.simpleNames.single()} {
private String foo;
private String bar;
public String getFoo() { return foo; }
@@ -1981,11 +1980,11 @@
runProcessorTest(
listOf(
Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
import androidx.room.*;
- public class ${MY_POJO.simpleName()} {
+ public class ${MY_POJO.simpleNames.single()} {
private String foo;
private String bar;
public void setFoo(String foo) { this.foo = foo; }
@@ -2012,11 +2011,11 @@
runProcessorTest(
listOf(
Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
import androidx.room.*;
- public class ${MY_POJO.simpleName()} {
+ public class ${MY_POJO.simpleNames.single()} {
private String foo;
private String bar;
public void setFoo(String foo) { this.foo = foo; }
@@ -2043,11 +2042,11 @@
runProcessorTest(
listOf(
Source.java(
- MY_POJO.toString(),
+ MY_POJO.canonicalName,
"""
package foo.bar;
import androidx.room.*;
- public class ${MY_POJO.simpleName()} {
+ public class ${MY_POJO.simpleNames.single()} {
private String foo;
private String bar;
public void setFoo(String foo) { this.foo = foo; }
@@ -2187,7 +2186,7 @@
classpath: List<File> = emptyList(),
handler: (Pojo, XTestInvocation) -> Unit
) {
- val pojoSource = Source.java(MY_POJO.toString(), code)
+ val pojoSource = Source.java(MY_POJO.canonicalName, code)
val all = sources.toList() + pojoSource
runProcessorTest(
sources = all,
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
index f37014e..768c1a1 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
@@ -30,6 +30,7 @@
import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.ext.CommonTypeNames
import androidx.room.ext.CommonTypeNames.LIST
+import androidx.room.ext.CommonTypeNames.MUTABLE_LIST
import androidx.room.ext.CommonTypeNames.STRING
import androidx.room.ext.GuavaUtilConcurrentTypeNames
import androidx.room.ext.KotlinTypeNames
@@ -58,10 +59,6 @@
import androidx.room.vo.Warning
import androidx.room.vo.WriteQueryMethod
import com.google.common.truth.Truth.assertThat
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeVariableName
import createVerifierFromEntitiesAndViews
import mockElementAndType
import org.hamcrest.CoreMatchers.hasItem
@@ -77,7 +74,6 @@
import org.junit.runners.Parameterized
import org.mockito.Mockito
-@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
@RunWith(Parameterized::class)
class QueryMethodProcessorTest(private val enableVerification: Boolean) {
companion object {
@@ -155,7 +151,7 @@
assertThat(param.sqlName, `is`("x"))
assertThat(
param.type,
- `is`(invocation.processingEnv.requireType(TypeName.INT))
+ `is`(invocation.processingEnv.requireType(XTypeName.PRIMITIVE_INT))
)
}
}
@@ -339,11 +335,10 @@
abstract public <T> ${LIST.canonicalName}<T> foo(int x);
"""
) { parsedQuery, invocation ->
- val expected: TypeName = ParameterizedTypeName.get(
- ClassName.get(List::class.java),
- TypeVariableName.get("T")
+ val expected = MUTABLE_LIST.parametrizedBy(
+ XClassName.get("", "T")
)
- assertThat(parsedQuery.returnType.typeName, `is`(expected))
+ assertThat(parsedQuery.returnType.asTypeName(), `is`(expected))
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS
@@ -520,7 +515,7 @@
assertThat(parsedQuery.returnType.asTypeName(), `is`(XTypeName.UNIT_VOID))
assertThat(
parsedQuery.parameters.first().type.asTypeName(),
- `is`(CommonTypeNames.STRING)
+ `is`(STRING)
)
}
}
@@ -536,10 +531,7 @@
assertThat(parsedQuery.element.jvmName, `is`("insertUsername"))
assertThat(parsedQuery.parameters.size, `is`(1))
assertThat(parsedQuery.returnType.asTypeName(), `is`(XTypeName.UNIT_VOID))
- assertThat(
- parsedQuery.parameters.first().type.asTypeName(),
- `is`(CommonTypeNames.STRING)
- )
+ assertThat(parsedQuery.parameters.first().type.asTypeName(), `is`(STRING))
}
}
@@ -554,10 +546,7 @@
assertThat(parsedQuery.element.jvmName, `is`("insertUsername"))
assertThat(parsedQuery.parameters.size, `is`(1))
assertThat(parsedQuery.returnType.asTypeName(), `is`(XTypeName.PRIMITIVE_LONG))
- assertThat(
- parsedQuery.parameters.first().type.asTypeName(),
- `is`(CommonTypeNames.STRING)
- )
+ assertThat(parsedQuery.parameters.first().type.asTypeName(), `is`(STRING))
}
}
@@ -812,10 +801,9 @@
`is`(COMMON.NOT_AN_ENTITY_TYPE_NAME)
)
val adapter = parsedQuery.queryResultBinder.adapter
- assertThat(checkNotNull(adapter))
+ checkNotNull(adapter)
assertThat(adapter::class, `is`(SingleItemQueryResultAdapter::class))
val rowAdapter = adapter.rowAdapters.single()
- assertThat(checkNotNull(rowAdapter))
assertThat(rowAdapter::class, `is`(PojoRowAdapter::class))
}
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
index 9e64549..d6d58c9 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
@@ -153,7 +153,7 @@
singleQueryMethod(
"""
@RawQuery
- abstract public ${PagingTypeNames.POSITIONAL_DATA_SOURCE}<User> getOne();
+ abstract public ${PagingTypeNames.POSITIONAL_DATA_SOURCE.canonicalName}<User> getOne();
"""
) { _, invocation ->
invocation.assertCompilationResult {
@@ -169,7 +169,7 @@
singleQueryMethod(
"""
@RawQuery(observedEntities = {User.class})
- abstract public ${PagingTypeNames.POSITIONAL_DATA_SOURCE}<User> getOne(
+ abstract public ${PagingTypeNames.POSITIONAL_DATA_SOURCE.canonicalName}<User> getOne(
SupportSQLiteQuery query);
"""
) { _, _ ->
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
index 82733cd..a7291ea 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
@@ -35,7 +35,6 @@
import androidx.room.vo.Pojo
import androidx.room.vo.columnNames
import com.google.common.truth.Truth.assertThat
-import com.squareup.javapoet.TypeName
import org.hamcrest.CoreMatchers.hasItems
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
@@ -63,7 +62,7 @@
)
assertThat(entity.fields.size, `is`(1))
val field = entity.fields.first()
- val intType = invocation.processingEnv.requireType(TypeName.INT)
+ val intType = invocation.processingEnv.requireType(XTypeName.PRIMITIVE_INT)
assertThat(
field,
`is`(
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/BasicColumnTypeAdaptersTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/BasicColumnTypeAdaptersTest.kt
index 7ef769c..e582e54 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/BasicColumnTypeAdaptersTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/BasicColumnTypeAdaptersTest.kt
@@ -16,18 +16,21 @@
package androidx.room.solver
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.VisibilityModifier
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XFunSpec
+import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.XTypeSpec
+import androidx.room.compiler.codegen.asClassName
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.util.XTestInvocation
import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.compiler.processing.writeTo
+import androidx.room.ext.AndroidTypeNames
import androidx.room.processor.Context
import androidx.room.vo.BuiltInConverterFlags
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.JavaFile
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
@@ -37,60 +40,58 @@
@RunWith(Parameterized::class)
class BasicColumnTypeAdaptersTest(
- val input: TypeName,
+ val input: XTypeName,
val bindCode: String,
val cursorCode: String
) {
companion object {
- val SQLITE_STMT: TypeName = ClassName.get("android.database.sqlite", "SQLiteStatement")
- val CURSOR: TypeName = ClassName.get("android.database", "Cursor")
@Parameterized.Parameters(name = "kind:{0},bind:_{1},cursor:_{2}")
@JvmStatic
fun params(): List<Array<Any>> {
return listOf(
arrayOf(
- TypeName.INT,
+ XTypeName.PRIMITIVE_INT,
"st.bindLong(6, inp);",
"out = crs.getInt(9);"
),
arrayOf(
- TypeName.BYTE,
+ XTypeName.PRIMITIVE_BYTE,
"st.bindLong(6, inp);",
"out = (byte) (crs.getShort(9));"
),
arrayOf(
- TypeName.SHORT,
+ XTypeName.PRIMITIVE_SHORT,
"st.bindLong(6, inp);",
"out = crs.getShort(9);"
),
arrayOf(
- TypeName.LONG,
+ XTypeName.PRIMITIVE_LONG,
"st.bindLong(6, inp);",
"out = crs.getLong(9);"
),
arrayOf(
- TypeName.CHAR,
+ XTypeName.PRIMITIVE_CHAR,
"st.bindLong(6, inp);",
"out = (char) (crs.getInt(9));"
),
arrayOf(
- TypeName.FLOAT,
+ XTypeName.PRIMITIVE_FLOAT,
"st.bindDouble(6, inp);",
"out = crs.getFloat(9);"
),
arrayOf(
- TypeName.DOUBLE,
+ XTypeName.PRIMITIVE_DOUBLE,
"st.bindDouble(6, inp);",
"out = crs.getDouble(9);"
),
arrayOf(
- TypeName.get(String::class.java),
+ String::class.asClassName(),
"st.bindString(6, inp);",
"out = crs.getString(9);"
),
arrayOf(
- TypeName.get(ByteArray::class.java),
+ XTypeName.getArrayName(XTypeName.PRIMITIVE_BYTE),
"st.bindBlob(6, inp);",
"out = crs.getBlob(9);"
)
@@ -124,7 +125,7 @@
""".trimIndent()
}
adapter.bindToStmt("st", "6", "inp", scope)
- assertThat(scope.builder().build().toString().trim(), `is`(expected))
+ assertThat(scope.generate().toString().trim(), `is`(expected))
generateCode(invocation, scope, type)
}
}
@@ -155,7 +156,7 @@
""".trimIndent()
}
assertThat(
- scope.builder().build().toString().trim(),
+ scope.generate().toString().trim(),
`is`(
expected
)
@@ -180,7 +181,7 @@
)!!
adapter.bindToStmt("st", "6", "inp", scope)
assertThat(
- scope.builder().build().toString().trim(),
+ scope.generate().toString().trim(),
`is`(
"""
if (inp == null) {
@@ -200,20 +201,52 @@
// guard against multi round
return
}
- val spec = TypeSpec.classBuilder("OutClass")
- .addField(FieldSpec.builder(SQLITE_STMT, "st").build())
- .addField(FieldSpec.builder(CURSOR, "crs").build())
- .addField(FieldSpec.builder(type.typeName, "out").build())
- .addField(FieldSpec.builder(type.typeName, "inp").build())
- .addMethod(
- MethodSpec.methodBuilder("foo")
- .addCode(scope.builder().build())
+ XTypeSpec.classBuilder(
+ language = CodeLanguage.JAVA,
+ className = XClassName.get("foo.bar", "OuterClass")
+ ).apply {
+ addProperty(
+ XPropertySpec.builder(
+ language = CodeLanguage.JAVA,
+ name = "st",
+ typeName = XClassName.get("android.database.sqlite", "SQLiteStatement"),
+ visibility = VisibilityModifier.PUBLIC,
+ isMutable = true
+ ).build()
+ )
+ addProperty(
+ XPropertySpec.builder(
+ language = CodeLanguage.JAVA,
+ name = "crs",
+ typeName = AndroidTypeNames.CURSOR,
+ visibility = VisibilityModifier.PUBLIC,
+ isMutable = true
+ ).build()
+ )
+ addProperty(
+ XPropertySpec.builder(
+ language = CodeLanguage.JAVA,
+ name = "out",
+ typeName = type.asTypeName(),
+ visibility = VisibilityModifier.PUBLIC,
+ isMutable = true
+ ).build()
+ )
+ addProperty(
+ XPropertySpec.builder(
+ language = CodeLanguage.JAVA,
+ name = "inp",
+ typeName = type.asTypeName(),
+ visibility = VisibilityModifier.PUBLIC,
+ isMutable = true
+ ).build()
+ )
+ addFunction(
+ XFunSpec.builder(CodeLanguage.JAVA, "foo", VisibilityModifier.PUBLIC)
+ .addCode(scope.generate())
.build()
)
- .build()
- JavaFile.builder("foo.bar", spec).build().writeTo(
- invocation.processingEnv.filer
- )
+ }.build().writeTo(invocation.processingEnv.filer)
}
@Test
@@ -241,7 +274,7 @@
""".trimIndent()
}
adapter.readFromCursor("out", "crs", "9", scope)
- assertThat(scope.builder().build().toString().trim(), `is`(expected))
+ assertThat(scope.generate().toString().trim(), `is`(expected))
generateCode(invocation, scope, type)
}
}
@@ -272,7 +305,7 @@
""".trimIndent()
}
assertThat(
- scope.builder().build().toString().trim(),
+ scope.generate().toString().trim(),
`is`(
expected
)
@@ -292,7 +325,7 @@
).findColumnTypeAdapter(nullableType, null, false)!!
adapter.readFromCursor("out", "crs", "9", scope)
assertThat(
- scope.builder().build().toString().trim(),
+ scope.generate().toString().trim(),
`is`(
"""
if (crs.isNull(9)) {
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
index 009d64c..84d9666 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -22,6 +22,7 @@
import androidx.room.Dao
import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.asClassName
import androidx.room.compiler.processing.XProcessingEnv
import androidx.room.compiler.processing.XRawType
import androidx.room.compiler.processing.isTypeElement
@@ -38,7 +39,6 @@
import androidx.room.ext.RxJava2TypeNames
import androidx.room.ext.RxJava3TypeNames
import androidx.room.ext.implementsEqualsAndHashcode
-import androidx.room.ext.typeName
import androidx.room.parser.SQLTypeAffinity
import androidx.room.processor.Context
import androidx.room.processor.CustomConverterProcessor
@@ -74,7 +74,6 @@
import androidx.room.vo.BuiltInConverterFlags
import androidx.room.vo.ReadQueryMethod
import com.google.common.truth.Truth.assertThat
-import com.squareup.javapoet.TypeName
import java.util.UUID
import org.hamcrest.CoreMatchers.instanceOf
import org.hamcrest.CoreMatchers.`is`
@@ -86,7 +85,6 @@
import org.junit.runners.JUnit4
import testCodeGenScope
-@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
@RunWith(JUnit4::class)
class TypeAdapterStoreTest {
companion object {
@@ -155,7 +153,7 @@
Context(invocation.processingEnv),
BuiltInConverterFlags.DEFAULT
)
- val primitiveType = invocation.processingEnv.requireType(TypeName.INT)
+ val primitiveType = invocation.processingEnv.requireType(XTypeName.PRIMITIVE_INT)
val adapter = store.findColumnTypeAdapter(
primitiveType,
null,
@@ -250,7 +248,7 @@
)
val uuid = invocation
.processingEnv
- .requireType(UUID::class.typeName)
+ .requireType(UUID::class.asClassName())
val adapter = store.findColumnTypeAdapter(
out = uuid,
affinity = null,
@@ -269,7 +267,7 @@
Context(invocation.processingEnv),
BuiltInConverterFlags.DEFAULT
)
- val booleanType = invocation.processingEnv.requireType(TypeName.BOOLEAN)
+ val booleanType = invocation.processingEnv.requireType(XTypeName.PRIMITIVE_BOOLEAN)
val adapter = store.findColumnTypeAdapter(
booleanType,
null,
@@ -280,7 +278,7 @@
val bindScope = testCodeGenScope()
adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope)
assertThat(
- bindScope.builder().build().toString().trim(),
+ bindScope.generate().toString().trim(),
`is`(
"""
final int ${tmp(0)} = fooVar ? 1 : 0;
@@ -292,7 +290,7 @@
val cursorScope = testCodeGenScope()
adapter.readFromCursor("res", "curs", "7", cursorScope)
assertThat(
- cursorScope.builder().build().toString().trim(),
+ cursorScope.generate().toString().trim(),
`is`(
"""
final int ${tmp(0)};
@@ -355,7 +353,7 @@
val bindScope = testCodeGenScope()
adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope)
assertThat(
- bindScope.builder().build().toString().trim(),
+ bindScope.generate().toString().trim(),
`is`(
"""
final boolean ${tmp(0)} = foo.bar.Point.toBoolean(fooVar);
@@ -368,7 +366,7 @@
val cursorScope = testCodeGenScope()
adapter.readFromCursor("res", "curs", "11", cursorScope).toString()
assertThat(
- cursorScope.builder().build().toString().trim(),
+ cursorScope.generate().toString().trim(),
`is`(
"""
final int ${tmp(0)};
@@ -396,7 +394,7 @@
val bindScope = testCodeGenScope()
adapter!!.readFromCursor("outDate", "curs", "0", bindScope)
assertThat(
- bindScope.builder().build().toString().trim(),
+ bindScope.generate().toString().trim(),
`is`(
"""
final java.lang.Long _tmp;
@@ -445,7 +443,7 @@
}
""".trimIndent()
}
- assertThat(bindScope.builder().build().toString().trim()).isEqualTo(
+ assertThat(bindScope.generate().toString().trim()).isEqualTo(
"""
|final java.lang.String ${tmp(0)} = androidx.room.util.StringUtil.joinIntoString(fooVar);
|$expectedAdapterCode
@@ -491,7 +489,6 @@
@Test
fun testMissingRx2Room() {
- @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
runProcessorTest(
sources = listOf(COMMON.PUBLISHER, COMMON.RX2_FLOWABLE)
) { invocation ->
@@ -512,7 +509,6 @@
@Test
fun testMissingRx3Room() {
- @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
runProcessorTest(
sources = listOf(COMMON.PUBLISHER, COMMON.RX3_FLOWABLE)
) { invocation ->
@@ -627,7 +623,6 @@
COMMON.RX2_FLOWABLE to COMMON.RX2_ROOM,
COMMON.RX3_FLOWABLE to COMMON.RX3_ROOM
).forEach { (rxTypeSrc, rxRoomSrc) ->
- @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
runProcessorTest(
sources = listOf(
COMMON.RX2_SINGLE,
@@ -658,7 +653,6 @@
Triple(COMMON.RX2_FLOWABLE, COMMON.RX2_ROOM, RxJava2TypeNames.FLOWABLE),
Triple(COMMON.RX3_FLOWABLE, COMMON.RX3_ROOM, RxJava3TypeNames.FLOWABLE)
).forEach { (rxTypeSrc, rxRoomSrc, rxTypeClassName) ->
- @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
runProcessorTest(
sources = listOf(
COMMON.RX2_SINGLE,
@@ -687,7 +681,6 @@
Triple(COMMON.RX2_OBSERVABLE, COMMON.RX2_ROOM, RxJava2TypeNames.OBSERVABLE),
Triple(COMMON.RX3_OBSERVABLE, COMMON.RX3_ROOM, RxJava3TypeNames.OBSERVABLE)
).forEach { (rxTypeSrc, rxRoomSrc, rxTypeClassName) ->
- @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
runProcessorTest(
sources = listOf(
COMMON.RX2_SINGLE,
@@ -717,7 +710,6 @@
Triple(COMMON.RX2_SINGLE, COMMON.RX2_ROOM, RxJava2TypeNames.SINGLE),
Triple(COMMON.RX3_SINGLE, COMMON.RX3_ROOM, RxJava3TypeNames.SINGLE)
).forEach { (rxTypeSrc, _, rxTypeClassName) ->
- @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
runProcessorTest(sources = listOf(rxTypeSrc)) { invocation ->
val single = invocation.processingEnv.requireTypeElement(rxTypeClassName)
assertThat(single, notNullValue())
@@ -849,7 +841,6 @@
Triple(COMMON.RX2_SINGLE, COMMON.RX2_ROOM, RxJava2TypeNames.SINGLE),
Triple(COMMON.RX3_SINGLE, COMMON.RX3_ROOM, RxJava3TypeNames.SINGLE)
).forEach { (rxTypeSrc, _, rxTypeClassName) ->
- @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
runProcessorTest(sources = listOf(rxTypeSrc)) { invocation ->
val single = invocation.processingEnv.requireTypeElement(rxTypeClassName)
assertThat(single).isNotNull()
@@ -1462,7 +1453,7 @@
collectionTypes.map { collectionType ->
invocation.processingEnv.getDeclaredType(
invocation.processingEnv.requireTypeElement(collectionType),
- invocation.processingEnv.requireType(TypeName.INT).boxed()
+ invocation.processingEnv.requireType(XTypeName.PRIMITIVE_INT).boxed()
)
}.forEach { type ->
val adapter = store.findQueryParameterAdapter(
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt
index 5279228..e334883 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt
@@ -58,7 +58,7 @@
) { _, writer ->
val scope = testCodeGenScope()
writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.builder().build().toString().trim()).isEqualTo(
+ assertThat(scope.generate().toString().trim()).isEqualTo(
"""
final java.lang.String _sql = "SELECT id FROM users";
final $QUERY _stmt = $QUERY.acquire(_sql, 0);
@@ -90,7 +90,7 @@
}
""".trimIndent()
}
- assertThat(scope.builder().build().toString().trim()).isEqualTo(
+ assertThat(scope.generate().toString().trim()).isEqualTo(
"""
|final java.lang.String _sql = "SELECT id FROM users WHERE name LIKE ?";
|final $QUERY _stmt = $QUERY.acquire(_sql, 1);
@@ -111,7 +111,7 @@
) { _, writer ->
val scope = testCodeGenScope()
writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.builder().build().toString().trim()).isEqualTo(
+ assertThat(scope.generate().toString().trim()).isEqualTo(
"""
final java.lang.String _sql = "SELECT id FROM users WHERE id IN(?,?)";
final $QUERY _stmt = $QUERY.acquire(_sql, 2);
@@ -134,7 +134,7 @@
) { _, writer ->
val scope = testCodeGenScope()
writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.builder().build().toString().trim()).isEqualTo(
+ assertThat(scope.generate().toString().trim()).isEqualTo(
"""
final java.lang.StringBuilder _stringBuilder = ${STRING_UTIL.canonicalName}.newStringBuilder();
_stringBuilder.append("SELECT id FROM users WHERE id IN(");
@@ -198,7 +198,7 @@
) { _, writer ->
val scope = testCodeGenScope()
writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.builder().build().toString().trim()).isEqualTo(collectionOut)
+ assertThat(scope.generate().toString().trim()).isEqualTo(collectionOut)
}
}
@@ -212,7 +212,7 @@
) { _, writer ->
val scope = testCodeGenScope()
writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.builder().build().toString().trim()).isEqualTo(collectionOut)
+ assertThat(scope.generate().toString().trim()).isEqualTo(collectionOut)
}
}
@@ -226,7 +226,7 @@
) { _, writer ->
val scope = testCodeGenScope()
writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.builder().build().toString().trim()).isEqualTo(collectionOut)
+ assertThat(scope.generate().toString().trim()).isEqualTo(collectionOut)
}
}
@@ -240,7 +240,7 @@
) { _, writer ->
val scope = testCodeGenScope()
writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.builder().build().toString().trim()).isEqualTo(
+ assertThat(scope.generate().toString().trim()).isEqualTo(
"""
final java.lang.String _sql = "SELECT id FROM users WHERE age > ? OR bage > ?";
final $QUERY _stmt = $QUERY.acquire(_sql, 2);
@@ -263,7 +263,7 @@
) { _, writer ->
val scope = testCodeGenScope()
writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.builder().build().toString().trim()).isEqualTo(
+ assertThat(scope.generate().toString().trim()).isEqualTo(
"""
final java.lang.StringBuilder _stringBuilder = ${STRING_UTIL.canonicalName}.newStringBuilder();
_stringBuilder.append("SELECT id FROM users WHERE age > ");
@@ -305,7 +305,7 @@
) { _, writer ->
val scope = testCodeGenScope()
writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.builder().build().toString().trim()).isEqualTo(
+ assertThat(scope.generate().toString().trim()).isEqualTo(
"""
final java.lang.StringBuilder _stringBuilder = ${STRING_UTIL.canonicalName}.newStringBuilder();
_stringBuilder.append("SELECT id FROM users WHERE age IN (");
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
index 73af03b..dd79611f 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
@@ -16,6 +16,7 @@
package androidx.room.verifier
+import androidx.room.compiler.codegen.XTypeName
import androidx.room.compiler.processing.XConstructorElement
import androidx.room.compiler.processing.XElement
import androidx.room.compiler.processing.XFieldElement
@@ -38,7 +39,6 @@
import androidx.room.vo.FieldSetter
import androidx.room.vo.Fields
import androidx.room.vo.PrimaryKey
-import com.squareup.javapoet.TypeName
import java.sql.Connection
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.hasItem
@@ -312,7 +312,7 @@
"User",
field(
"id",
- primitive(invocation.context, TypeName.INT),
+ primitive(invocation.context, XTypeName.PRIMITIVE_INT),
SQLTypeAffinity.INTEGER
),
field(
@@ -348,16 +348,19 @@
entity(
invocation,
"User",
- field("id", primitive(context, TypeName.INT), SQLTypeAffinity.INTEGER),
+ field("id",
+ primitive(context, XTypeName.PRIMITIVE_INT), SQLTypeAffinity.INTEGER),
field("name", context.COMMON_TYPES.STRING, SQLTypeAffinity.TEXT),
field("lastName", context.COMMON_TYPES.STRING, SQLTypeAffinity.TEXT),
- field("ratio", primitive(context, TypeName.FLOAT), SQLTypeAffinity.REAL)
+ field("ratio",
+ primitive(context, XTypeName.PRIMITIVE_FLOAT), SQLTypeAffinity.REAL)
)
),
listOf(
view(
"UserSummary", "SELECT id, name FROM User",
- field("id", primitive(context, TypeName.INT), SQLTypeAffinity.INTEGER),
+ field("id",
+ primitive(context, XTypeName.PRIMITIVE_INT), SQLTypeAffinity.INTEGER),
field("name", context.COMMON_TYPES.STRING, SQLTypeAffinity.TEXT)
)
)
@@ -439,7 +442,7 @@
f.setter = FieldSetter(f.name, name, type, CallType.FIELD)
}
- private fun primitive(context: Context, typeName: TypeName): XType {
+ private fun primitive(context: Context, typeName: XTypeName): XType {
return context.processingEnv.requireType(typeName)
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
index 75a5d4c..e39639d 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
@@ -1074,12 +1074,19 @@
}
@Transaction
- open fun concreteInternal() {
+ internal open fun concreteInternal() {
}
@Transaction
open suspend fun suspendConcrete() {
+ }
+ @Transaction
+ open fun concreteWithVararg(vararg arr: Long) {
+ }
+
+ @Transaction
+ open suspend fun suspendConcreteWithVararg(vararg arr: Long) {
}
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutines.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutines.kt
index 0a6dff1..f775cf2 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutines.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutines.kt
@@ -15,7 +15,6 @@
import java.util.ArrayList
import java.util.concurrent.Callable
import javax.`annotation`.processing.Generated
-import kotlin.Array
import kotlin.Int
import kotlin.String
import kotlin.Suppress
@@ -128,7 +127,7 @@
})
}
- public override suspend fun getSuspendList(arg: Array<out String?>): List<MyEntity> {
+ public override suspend fun getSuspendList(vararg arg: String?): List<MyEntity> {
val _stringBuilder: StringBuilder = newStringBuilder()
_stringBuilder.append("SELECT * FROM MyEntity WHERE pk IN (")
val _inputSize: Int = arg.size
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/shortcutMethods_suspend.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/shortcutMethods_suspend.kt
index 7362f6a..de09864 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/shortcutMethods_suspend.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/shortcutMethods_suspend.kt
@@ -7,7 +7,6 @@
import java.lang.Class
import java.util.concurrent.Callable
import javax.`annotation`.processing.Generated
-import kotlin.Array
import kotlin.Int
import kotlin.Long
import kotlin.String
@@ -79,7 +78,7 @@
})
}
- public override suspend fun insert(entities: Array<out MyEntity>): List<Long> =
+ public override suspend fun insert(vararg entities: MyEntity): List<Long> =
CoroutinesRoom.execute(__db, true, object : Callable<List<Long>> {
public override fun call(): List<Long> {
__db.beginTransaction()
@@ -123,7 +122,7 @@
}
})
- public override suspend fun upsert(entities: Array<out MyEntity>): List<Long> =
+ public override suspend fun upsert(vararg entities: MyEntity): List<Long> =
CoroutinesRoom.execute(__db, true, object : Callable<List<Long>> {
public override fun call(): List<Long> {
__db.beginTransaction()
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt
index 3db73a3..d59e245 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt
@@ -2,6 +2,7 @@
import androidx.room.withTransaction
import java.lang.Class
import javax.`annotation`.processing.Generated
+import kotlin.Long
import kotlin.Suppress
import kotlin.Unit
import kotlin.collections.List
@@ -43,7 +44,7 @@
}
}
- public override fun concreteInternal(): Unit {
+ internal override fun concreteInternal(): Unit {
__db.beginTransaction()
try {
super@MyDao_Impl.concreteInternal()
@@ -59,6 +60,22 @@
}
}
+ public override fun concreteWithVararg(vararg arr: Long): Unit {
+ __db.beginTransaction()
+ try {
+ super@MyDao_Impl.concreteWithVararg(*arr)
+ __db.setTransactionSuccessful()
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override suspend fun suspendConcreteWithVararg(vararg arr: Long): Unit {
+ __db.withTransaction {
+ super@MyDao_Impl.suspendConcreteWithVararg(*arr)
+ }
+ }
+
public companion object {
@JvmStatic
public fun getRequiredConverters(): List<Class<*>> = emptyList()
diff --git a/settings.gradle b/settings.gradle
index a098059..2471fc2d 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -365,8 +365,9 @@
includeProject(":annotation:annotation-experimental-lint")
includeProject(":annotation:annotation-experimental-lint-integration-tests", "annotation/annotation-experimental-lint/integration-tests")
includeProject(":annotation:annotation-sampled")
-includeProject(":appactions:interaction:interaction-proto", [BuildType.MAIN])
includeProject(":appactions:interaction:interaction-capabilities-core", [BuildType.MAIN])
+includeProject(":appactions:interaction:interaction-proto", [BuildType.MAIN])
+includeProject(":appactions:interaction:interaction-service", [BuildType.MAIN])
includeProject(":appcompat:appcompat", [BuildType.MAIN])
includeProject(":appcompat:appcompat-benchmark", [BuildType.MAIN])
includeProject(":appcompat:appcompat-lint", [BuildType.MAIN])
@@ -382,8 +383,8 @@
includeProject(":appsearch:appsearch-local-storage", [BuildType.MAIN])
includeProject(":appsearch:appsearch-platform-storage", [BuildType.MAIN])
includeProject(":appsearch:appsearch-test-util", [BuildType.MAIN])
-includeProject(":arch:core:core-common", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":arch:core:core-runtime", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":arch:core:core-common", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":arch:core:core-runtime", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
includeProject(":arch:core:core-testing", [BuildType.MAIN])
includeProject(":asynclayoutinflater:asynclayoutinflater", [BuildType.MAIN])
includeProject(":asynclayoutinflater:asynclayoutinflater-appcompat", [BuildType.MAIN])
@@ -395,9 +396,17 @@
includeProject(":benchmark:benchmark-darwin-samples", [BuildType.KMP])
includeProject(":benchmark:benchmark-darwin-gradle-plugin", [BuildType.KMP])
includeProject(":benchmark:benchmark-gradle-plugin", "benchmark/gradle-plugin", [BuildType.MAIN])
+includeProject(":benchmark:benchmark-baseline-profiles-gradle-plugin", "benchmark/baseline-profiles-gradle-plugin",[BuildType.MAIN])
includeProject(":benchmark:benchmark-junit4")
includeProject(":benchmark:benchmark-macro", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":benchmark:benchmark-macro-junit4", [BuildType.MAIN, BuildType.COMPOSE])
+includeProject(":benchmark:integration-tests:baselineprofiles-producer", [BuildType.MAIN])
+includeProject(":benchmark:integration-tests:baselineprofiles-consumer", [BuildType.MAIN])
+includeProject(":benchmark:integration-tests:baselineprofiles-flavors-producer", [BuildType.MAIN])
+includeProject(":benchmark:integration-tests:baselineprofiles-flavors-consumer", [BuildType.MAIN])
+includeProject(":benchmark:integration-tests:baselineprofiles-library-consumer", [BuildType.MAIN])
+includeProject(":benchmark:integration-tests:baselineprofiles-library-producer", [BuildType.MAIN])
+includeProject(":benchmark:integration-tests:baselineprofiles-library-build-provider", [BuildType.MAIN])
includeProject(":benchmark:integration-tests:dry-run-benchmark", [BuildType.MAIN])
includeProject(":benchmark:integration-tests:macrobenchmark", [BuildType.MAIN])
includeProject(":benchmark:integration-tests:macrobenchmark-target", [BuildType.MAIN])
@@ -714,33 +723,33 @@
includeProject(":lifecycle:integration-tests:incrementality", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:integration-tests:lifecycle-testapp", "lifecycle/integration-tests/testapp", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:integration-tests:lifecycle-testapp-kotlin", "lifecycle/integration-tests/kotlintestapp", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-common", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-common-java8", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-common", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":lifecycle:lifecycle-common-java8", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
includeProject(":lifecycle:lifecycle-compiler", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:lifecycle-extensions", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-livedata", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-livedata-core", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-livedata-core-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-livedata-core-ktx-lint", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-livedata", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":lifecycle:lifecycle-livedata-core", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":lifecycle:lifecycle-livedata-core-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":lifecycle:lifecycle-livedata-core-ktx-lint", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
includeProject(":lifecycle:lifecycle-livedata-core-truth", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-livedata-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-livedata-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
includeProject(":lifecycle:lifecycle-process", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:lifecycle-reactivestreams", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:lifecycle-reactivestreams-ktx", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-runtime", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-runtime", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
includeProject(":lifecycle:lifecycle-runtime-compose", [BuildType.COMPOSE])
includeProject(":lifecycle:lifecycle-runtime-compose:lifecycle-runtime-compose-samples", "lifecycle/lifecycle-runtime-compose/samples", [BuildType.COMPOSE])
includeProject(":lifecycle:lifecycle-runtime-compose:integration-tests:lifecycle-runtime-compose-demos", [BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-runtime-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-runtime-ktx-lint", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-runtime-testing", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-runtime-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":lifecycle:lifecycle-runtime-ktx-lint", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":lifecycle:lifecycle-runtime-testing", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
includeProject(":lifecycle:lifecycle-service", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-viewmodel", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-viewmodel", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
includeProject(":lifecycle:lifecycle-viewmodel-compose", [BuildType.COMPOSE])
includeProject(":lifecycle:lifecycle-viewmodel-compose:lifecycle-viewmodel-compose-samples", "lifecycle/lifecycle-viewmodel-compose/samples", [BuildType.COMPOSE])
includeProject(":lifecycle:lifecycle-viewmodel-compose:integration-tests:lifecycle-viewmodel-demos", [BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-viewmodel-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-viewmodel-savedstate", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-viewmodel-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":lifecycle:lifecycle-viewmodel-savedstate", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
includeProject(":lint-checks")
includeProject(":lint-checks:integration-tests")
includeProject(":loader:loader", [BuildType.MAIN])
diff --git a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/util/ProcessLock.kt b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/util/ProcessLock.kt
index 3b79342..dc07eb9 100644
--- a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/util/ProcessLock.kt
+++ b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/util/ProcessLock.kt
@@ -52,12 +52,12 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class ProcessLock(
name: String,
- lockDir: File,
+ lockDir: File?,
private val processLock: Boolean
) {
- private val lockFile: File = File(lockDir, "$name.lck")
+ private val lockFile: File? = lockDir?.let { File(it, "$name.lck") }
@SuppressLint("SyntheticAccessor")
- private val threadLock: Lock = getThreadLock(lockFile.absolutePath)
+ private val threadLock: Lock = getThreadLock(name)
private var lockChannel: FileChannel? = null
/**
@@ -69,6 +69,9 @@
threadLock.lock()
if (processLock) {
try {
+ if (lockFile == null) {
+ throw IOException("No lock directory was provided.")
+ }
// Verify parent dir
val parentDir = lockFile.parentFile
parentDir?.mkdirs()
diff --git a/test/uiautomator/uiautomator/api/current.txt b/test/uiautomator/uiautomator/api/current.txt
index 6cb3309..152c347 100644
--- a/test/uiautomator/uiautomator/api/current.txt
+++ b/test/uiautomator/uiautomator/api/current.txt
@@ -75,6 +75,10 @@
method public androidx.test.uiautomator.BySelector textStartsWith(String);
}
+ public interface Condition<T, U> {
+ method public U! apply(T!);
+ }
+
public final class Configurator {
method public long getActionAcknowledgmentTimeout();
method public static androidx.test.uiautomator.Configurator getInstance();
@@ -101,15 +105,16 @@
enum_constant public static final androidx.test.uiautomator.Direction UP;
}
- public abstract class EventCondition<U> {
+ public abstract class EventCondition<U> implements android.app.UiAutomation.AccessibilityEventFilter {
ctor public EventCondition();
+ method public abstract U! getResult();
}
public interface IAutomationSupport {
method public void sendStatus(int, android.os.Bundle);
}
- public abstract class SearchCondition<U> {
+ public abstract class SearchCondition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.Searchable,U> {
ctor public SearchCondition();
}
@@ -200,7 +205,7 @@
method public boolean takeScreenshot(java.io.File);
method public boolean takeScreenshot(java.io.File, float, int);
method public void unfreezeRotation() throws android.os.RemoteException;
- method public <U> U! wait(androidx.test.uiautomator.SearchCondition<U!>, long);
+ method public <U> U! wait(androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiDevice,U!>, long);
method public void waitForIdle();
method public void waitForIdle(long);
method public boolean waitForWindowUpdate(String?, long);
@@ -308,11 +313,10 @@
method public void setText(String?);
method public void swipe(androidx.test.uiautomator.Direction, float);
method public void swipe(androidx.test.uiautomator.Direction, float, int);
- method public <U> U! wait(androidx.test.uiautomator.UiObject2Condition<U!>, long);
- method public <U> U! wait(androidx.test.uiautomator.SearchCondition<U!>, long);
+ method public <U> U! wait(androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiObject2,U!>, long);
}
- public abstract class UiObject2Condition<U> {
+ public abstract class UiObject2Condition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.UiObject2,U> {
ctor public UiObject2Condition();
}
diff --git a/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt b/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt
index 6cb3309..152c347 100644
--- a/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt
+++ b/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt
@@ -75,6 +75,10 @@
method public androidx.test.uiautomator.BySelector textStartsWith(String);
}
+ public interface Condition<T, U> {
+ method public U! apply(T!);
+ }
+
public final class Configurator {
method public long getActionAcknowledgmentTimeout();
method public static androidx.test.uiautomator.Configurator getInstance();
@@ -101,15 +105,16 @@
enum_constant public static final androidx.test.uiautomator.Direction UP;
}
- public abstract class EventCondition<U> {
+ public abstract class EventCondition<U> implements android.app.UiAutomation.AccessibilityEventFilter {
ctor public EventCondition();
+ method public abstract U! getResult();
}
public interface IAutomationSupport {
method public void sendStatus(int, android.os.Bundle);
}
- public abstract class SearchCondition<U> {
+ public abstract class SearchCondition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.Searchable,U> {
ctor public SearchCondition();
}
@@ -200,7 +205,7 @@
method public boolean takeScreenshot(java.io.File);
method public boolean takeScreenshot(java.io.File, float, int);
method public void unfreezeRotation() throws android.os.RemoteException;
- method public <U> U! wait(androidx.test.uiautomator.SearchCondition<U!>, long);
+ method public <U> U! wait(androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiDevice,U!>, long);
method public void waitForIdle();
method public void waitForIdle(long);
method public boolean waitForWindowUpdate(String?, long);
@@ -308,11 +313,10 @@
method public void setText(String?);
method public void swipe(androidx.test.uiautomator.Direction, float);
method public void swipe(androidx.test.uiautomator.Direction, float, int);
- method public <U> U! wait(androidx.test.uiautomator.UiObject2Condition<U!>, long);
- method public <U> U! wait(androidx.test.uiautomator.SearchCondition<U!>, long);
+ method public <U> U! wait(androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiObject2,U!>, long);
}
- public abstract class UiObject2Condition<U> {
+ public abstract class UiObject2Condition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.UiObject2,U> {
ctor public UiObject2Condition();
}
diff --git a/test/uiautomator/uiautomator/api/restricted_current.txt b/test/uiautomator/uiautomator/api/restricted_current.txt
index 6cb3309..152c347 100644
--- a/test/uiautomator/uiautomator/api/restricted_current.txt
+++ b/test/uiautomator/uiautomator/api/restricted_current.txt
@@ -75,6 +75,10 @@
method public androidx.test.uiautomator.BySelector textStartsWith(String);
}
+ public interface Condition<T, U> {
+ method public U! apply(T!);
+ }
+
public final class Configurator {
method public long getActionAcknowledgmentTimeout();
method public static androidx.test.uiautomator.Configurator getInstance();
@@ -101,15 +105,16 @@
enum_constant public static final androidx.test.uiautomator.Direction UP;
}
- public abstract class EventCondition<U> {
+ public abstract class EventCondition<U> implements android.app.UiAutomation.AccessibilityEventFilter {
ctor public EventCondition();
+ method public abstract U! getResult();
}
public interface IAutomationSupport {
method public void sendStatus(int, android.os.Bundle);
}
- public abstract class SearchCondition<U> {
+ public abstract class SearchCondition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.Searchable,U> {
ctor public SearchCondition();
}
@@ -200,7 +205,7 @@
method public boolean takeScreenshot(java.io.File);
method public boolean takeScreenshot(java.io.File, float, int);
method public void unfreezeRotation() throws android.os.RemoteException;
- method public <U> U! wait(androidx.test.uiautomator.SearchCondition<U!>, long);
+ method public <U> U! wait(androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiDevice,U!>, long);
method public void waitForIdle();
method public void waitForIdle(long);
method public boolean waitForWindowUpdate(String?, long);
@@ -308,11 +313,10 @@
method public void setText(String?);
method public void swipe(androidx.test.uiautomator.Direction, float);
method public void swipe(androidx.test.uiautomator.Direction, float, int);
- method public <U> U! wait(androidx.test.uiautomator.UiObject2Condition<U!>, long);
- method public <U> U! wait(androidx.test.uiautomator.SearchCondition<U!>, long);
+ method public <U> U! wait(androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiObject2,U!>, long);
}
- public abstract class UiObject2Condition<U> {
+ public abstract class UiObject2Condition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.UiObject2,U> {
ctor public UiObject2Condition();
}
diff --git a/test/uiautomator/uiautomator/lint-baseline.xml b/test/uiautomator/uiautomator/lint-baseline.xml
index 1a52277..339bd83 100644
--- a/test/uiautomator/uiautomator/lint-baseline.xml
+++ b/test/uiautomator/uiautomator/lint-baseline.xml
@@ -1,5 +1,5 @@
<?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="BanUncheckedReflection"
@@ -12,6 +12,15 @@
<issue
id="LambdaLast"
+ message="Functional interface parameters (such as parameter 1, "condition", in androidx.test.uiautomator.UiDevice.wait) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions"
+ errorLine1=" public <U> U wait(@NonNull Condition<? super UiDevice, U> condition, long timeout) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/test/uiautomator/UiDevice.java"/>
+ </issue>
+
+ <issue
+ id="LambdaLast"
message="Functional interface parameters (such as parameter 1, "action", in androidx.test.uiautomator.UiDevice.performActionAndWait) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions"
errorLine1=" @NonNull EventCondition<U> condition, long timeout) {"
errorLine2=" ~~~~~~~~~~~~">
@@ -20,21 +29,12 @@
</issue>
<issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" protected void initializeUiAutomatorTest(UiAutomatorTestCase test) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ id="LambdaLast"
+ message="Functional interface parameters (such as parameter 1, "condition", in androidx.test.uiautomator.UiObject2.wait) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions"
+ errorLine1=" public <U> U wait(@NonNull Condition<? super UiObject2, U> condition, long timeout) {"
+ errorLine2=" ~~~~~~~~~~~~">
<location
- file="src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
- errorLine1=" protected AndroidTestRunner getAndroidTestRunner() {"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java"/>
+ file="src/main/java/androidx/test/uiautomator/UiObject2.java"/>
</issue>
</issues>
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Condition.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Condition.java
index 8958ddc..653d286 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Condition.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Condition.java
@@ -16,12 +16,12 @@
package androidx.test.uiautomator;
-/** Abstract class which represents a condition to be satisfied. */
-abstract class Condition<T, U> {
+/** Represents a condition to be satisfied. */
+public interface Condition<T, U> {
/**
* Applies the given arguments against this condition. Returns a non-null, non-false result if
* the condition is satisfied.
*/
- abstract U apply(T args);
+ U apply(T args);
}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/EventCondition.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/EventCondition.java
index 362e2b0..5d4cc69 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/EventCondition.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/EventCondition.java
@@ -16,14 +16,16 @@
package androidx.test.uiautomator;
-import android.view.accessibility.AccessibilityEvent;
+import android.app.UiAutomation.AccessibilityEventFilter;
/**
* An {@link EventCondition} is a condition which depends on an event or series of events having
* occurred.
*/
-public abstract class EventCondition<U> extends Condition<AccessibilityEvent, Boolean> {
+public abstract class EventCondition<U> implements AccessibilityEventFilter {
- @SuppressWarnings("HiddenAbstractMethod")
- abstract U getResult();
+ /**
+ * returns a value obtained after applying the condition to a series of events
+ */
+ public abstract U getResult();
}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/SearchCondition.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/SearchCondition.java
index c3b2318..90761db 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/SearchCondition.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/SearchCondition.java
@@ -17,5 +17,5 @@
package androidx.test.uiautomator;
/** A {@link SearchCondition} is a condition that is satisfied by searching for UI elements. */
-public abstract class SearchCondition<U> extends Condition<Searchable, U> {
+public abstract class SearchCondition<U> implements Condition<Searchable, U> {
}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
index 1e5d3bd..786de295 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
@@ -163,12 +163,12 @@
/**
* Waits for given the {@code condition} to be met.
*
- * @param condition The {@link SearchCondition} to evaluate.
+ * @param condition The {@link Condition} to evaluate.
* @param timeout Maximum amount of time to wait in milliseconds.
* @return The final result returned by the {@code condition}, or null if the {@code condition}
* was not met before the {@code timeout}.
*/
- public <U> U wait(@NonNull SearchCondition<U> condition, long timeout) {
+ public <U> U wait(@NonNull Condition<? super UiDevice, U> condition, long timeout) {
Log.d(TAG, String.format("Waiting %dms for %s.", timeout, condition));
return mWaitMixin.wait(condition, timeout);
}
@@ -188,7 +188,7 @@
condition));
try {
event = getUiAutomation().executeAndWaitForEvent(
- action, new EventForwardingFilter(condition), timeout);
+ action, condition, timeout);
} catch (TimeoutException e) {
// Ignore
Log.w(TAG, String.format("Timed out waiting %dms on the condition.", timeout));
@@ -201,22 +201,6 @@
return condition.getResult();
}
- /** Proxy class which acts as an {@link AccessibilityEventFilter} and forwards calls to an
- * {@link EventCondition} instance. */
- private static class EventForwardingFilter implements AccessibilityEventFilter {
- private final EventCondition<?> mCondition;
-
- public EventForwardingFilter(EventCondition<?> condition) {
- mCondition = condition;
- }
-
- @Override
- public boolean accept(AccessibilityEvent event) {
- // Guard against nulls
- return Boolean.TRUE.equals(mCondition.apply(event));
- }
- }
-
/**
* Enables or disables layout hierarchy compression.
*
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
index 1bdd9dd..5fa391a 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
@@ -147,25 +147,12 @@
/**
* Waits for a {@code condition} to be met.
*
- * @param condition The {@link UiObject2Condition} to wait for.
+ * @param condition The {@link Condition} to evaluate.
* @param timeout The maximum time in milliseconds to wait for.
* @return The final result returned by the {@code condition}, or {@code null} if the {@code
* condition} was not met before the {@code timeout}.
*/
- public <U> U wait(@NonNull UiObject2Condition<U> condition, long timeout) {
- Log.d(TAG, String.format("Waiting %dms for %s.", timeout, condition));
- return mWaitMixin.wait(condition, timeout);
- }
-
- /**
- * Waits for a {@code condition} to be met.
- *
- * @param condition The {@link SearchCondition} to evaluate.
- * @param timeout The maximum time in milliseconds to wait for.
- * @return The final result returned by the {@code condition}, or {@code null} if the {@code
- * condition} was not met before the {@code timeout}.
- */
- public <U> U wait(@NonNull SearchCondition<U> condition, long timeout) {
+ public <U> U wait(@NonNull Condition<? super UiObject2, U> condition, long timeout) {
Log.d(TAG, String.format("Waiting %dms for %s.", timeout, condition));
return mWaitMixin.wait(condition, timeout);
}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2Condition.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2Condition.java
index d1fbbff..372f5db 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2Condition.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2Condition.java
@@ -20,5 +20,5 @@
* A {@link UiObject2Condition} is a condition which is satisfied when a {@link UiObject2} is in a
* particular state.
*/
-public abstract class UiObject2Condition<U> extends Condition<UiObject2, U> {
+public abstract class UiObject2Condition<U> implements Condition<UiObject2, U> {
}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
index e980a1c..e349d96 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
@@ -41,7 +41,7 @@
public static SearchCondition<Boolean> gone(@NonNull BySelector selector) {
return new SearchCondition<Boolean>() {
@Override
- Boolean apply(Searchable container) {
+ public Boolean apply(Searchable container) {
return !container.hasObject(selector);
}
@@ -61,7 +61,7 @@
public static SearchCondition<Boolean> hasObject(@NonNull BySelector selector) {
return new SearchCondition<Boolean>() {
@Override
- Boolean apply(Searchable container) {
+ public Boolean apply(Searchable container) {
return container.hasObject(selector);
}
@@ -81,7 +81,7 @@
public static SearchCondition<UiObject2> findObject(@NonNull BySelector selector) {
return new SearchCondition<UiObject2>() {
@Override
- UiObject2 apply(Searchable container) {
+ public UiObject2 apply(Searchable container) {
return container.findObject(selector);
}
@@ -101,7 +101,7 @@
public static SearchCondition<List<UiObject2>> findObjects(@NonNull BySelector selector) {
return new SearchCondition<List<UiObject2>>() {
@Override
- List<UiObject2> apply(Searchable container) {
+ public List<UiObject2> apply(Searchable container) {
List<UiObject2> ret = container.findObjects(selector);
return ret.isEmpty() ? null : ret;
}
@@ -126,7 +126,7 @@
public static UiObject2Condition<Boolean> checkable(final boolean isCheckable) {
return new UiObject2Condition<Boolean>() {
@Override
- Boolean apply(UiObject2 object) {
+ public Boolean apply(UiObject2 object) {
return object.isCheckable() == isCheckable;
}
@@ -147,7 +147,7 @@
public static UiObject2Condition<Boolean> checked(final boolean isChecked) {
return new UiObject2Condition<Boolean>() {
@Override
- Boolean apply(UiObject2 object) {
+ public Boolean apply(UiObject2 object) {
return object.isChecked() == isChecked;
}
@@ -168,7 +168,7 @@
public static UiObject2Condition<Boolean> clickable(final boolean isClickable) {
return new UiObject2Condition<Boolean>() {
@Override
- Boolean apply(UiObject2 object) {
+ public Boolean apply(UiObject2 object) {
return object.isClickable() == isClickable;
}
@@ -189,7 +189,7 @@
public static UiObject2Condition<Boolean> enabled(final boolean isEnabled) {
return new UiObject2Condition<Boolean>() {
@Override
- Boolean apply(UiObject2 object) {
+ public Boolean apply(UiObject2 object) {
return object.isEnabled() == isEnabled;
}
@@ -210,7 +210,7 @@
public static UiObject2Condition<Boolean> focusable(final boolean isFocusable) {
return new UiObject2Condition<Boolean>() {
@Override
- Boolean apply(UiObject2 object) {
+ public Boolean apply(UiObject2 object) {
return object.isFocusable() == isFocusable;
}
@@ -231,7 +231,7 @@
public static UiObject2Condition<Boolean> focused(final boolean isFocused) {
return new UiObject2Condition<Boolean>() {
@Override
- Boolean apply(UiObject2 object) {
+ public Boolean apply(UiObject2 object) {
return object.isFocused() == isFocused;
}
@@ -252,7 +252,7 @@
public static UiObject2Condition<Boolean> longClickable(final boolean isLongClickable) {
return new UiObject2Condition<Boolean>() {
@Override
- Boolean apply(UiObject2 object) {
+ public Boolean apply(UiObject2 object) {
return object.isLongClickable() == isLongClickable;
}
@@ -273,7 +273,7 @@
public static UiObject2Condition<Boolean> scrollable(final boolean isScrollable) {
return new UiObject2Condition<Boolean>() {
@Override
- Boolean apply(UiObject2 object) {
+ public Boolean apply(UiObject2 object) {
return object.isScrollable() == isScrollable;
}
@@ -294,7 +294,7 @@
public static UiObject2Condition<Boolean> selected(final boolean isSelected) {
return new UiObject2Condition<Boolean>() {
@Override
- Boolean apply(UiObject2 object) {
+ public Boolean apply(UiObject2 object) {
return object.isSelected() == isSelected;
}
@@ -314,7 +314,7 @@
public static UiObject2Condition<Boolean> descMatches(@NonNull Pattern regex) {
return new UiObject2Condition<Boolean>() {
@Override
- Boolean apply(UiObject2 object) {
+ public Boolean apply(UiObject2 object) {
String desc = object.getContentDescription();
return regex.matcher(desc != null ? desc : "").matches();
}
@@ -379,7 +379,7 @@
public static UiObject2Condition<Boolean> textMatches(@NonNull Pattern regex) {
return new UiObject2Condition<Boolean>() {
@Override
- Boolean apply(UiObject2 object) {
+ public Boolean apply(UiObject2 object) {
String text = object.getText();
return regex.matcher(text != null ? text : "").matches();
}
@@ -408,7 +408,7 @@
public static UiObject2Condition<Boolean> textNotEquals(@NonNull String text) {
return new UiObject2Condition<Boolean>() {
@Override
- Boolean apply(UiObject2 object) {
+ public Boolean apply(UiObject2 object) {
return !text.equals(object.getText());
}
@@ -467,13 +467,13 @@
AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
@Override
- Boolean apply(AccessibilityEvent event) {
+ public boolean accept(AccessibilityEvent event) {
mMask &= ~event.getEventType();
return mMask == 0;
}
@Override
- Boolean getResult() {
+ public Boolean getResult() {
return mMask == 0;
}
@@ -497,7 +497,7 @@
private Boolean mResult = null;
@Override
- Boolean apply(AccessibilityEvent event) {
+ public boolean accept(AccessibilityEvent event) {
if (event.getEventType() != AccessibilityEvent.TYPE_VIEW_SCROLLED) {
return false; // Ignore non-scrolling events.
}
@@ -540,7 +540,7 @@
}
@Override
- Boolean getResult() {
+ public Boolean getResult() {
// If we didn't recieve any scroll events (mResult == null), assume we're already at
// the end and return true.
return mResult == null || mResult;
diff --git a/testutils/testutils-gradle-plugin/src/main/java/androidx/testutils/gradle/ProjectSetupRule.kt b/testutils/testutils-gradle-plugin/src/main/java/androidx/testutils/gradle/ProjectSetupRule.kt
index 23fc19d..7bd12a1c 100644
--- a/testutils/testutils-gradle-plugin/src/main/java/androidx/testutils/gradle/ProjectSetupRule.kt
+++ b/testutils/testutils-gradle-plugin/src/main/java/androidx/testutils/gradle/ProjectSetupRule.kt
@@ -28,8 +28,8 @@
*
* It should be used along side with SdkResourceGenerator in your build.gradle file
*/
-class ProjectSetupRule : ExternalResource() {
- val testProjectDir = TemporaryFolder()
+class ProjectSetupRule(parentFolder: File? = null) : ExternalResource() {
+ val testProjectDir = TemporaryFolder(parentFolder)
val props: ProjectProps by lazy { ProjectProps.load() }
diff --git a/tracing/OWNERS b/tracing/OWNERS
index 5db1872..9b3f90f 100644
--- a/tracing/OWNERS
+++ b/tracing/OWNERS
@@ -1,3 +1,4 @@
+# Bug component: 873508
ccraik@google.com
jgielzak@google.com
rahulrav@google.com
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index cc8da19..1e8c90f 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -189,7 +189,7 @@
}
public final class SurfaceKt {
- method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Surface(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional androidx.compose.foundation.BorderStroke? border, optional float tonalElevation, optional androidx.compose.ui.semantics.Role? role, optional float shadowElevation, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> getLocalAbsoluteTonalElevation();
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> LocalAbsoluteTonalElevation;
}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
index a5c80ea..7ce78931 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
@@ -14,57 +14,72 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalTvMaterial3Api::class)
-
package androidx.tv.material3
import android.os.Build
-import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.interaction.FocusInteraction
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.testutils.assertPixels
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.testutils.assertShape
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.click
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.test.pressKey
import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth
+import kotlin.math.abs
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+private fun assertFloatPrecision(a: Float, b: Float) =
+ Truth.assertThat(abs(a - b)).isLessThan(0.0001f)
+
+@OptIn(
+ ExperimentalComposeUiApi::class,
+ ExperimentalTestApi::class,
+ ExperimentalTvMaterial3Api::class
+)
@MediumTest
@RunWith(AndroidJUnit4::class)
class SurfaceTest {
@@ -72,233 +87,85 @@
@get:Rule
val rule = createComposeRule()
+ private fun Int.toDp(): Dp = with(rule.density) { toDp() }
+
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
- fun noTonalElevationColorIsSetOnNonElevatedSurfaceColor() {
- var absoluteTonalElevation: Dp = 0.dp
- var surfaceColor: Color = Color.Unspecified
- rule.setMaterialContent(lightColorScheme()) {
- surfaceColor = MaterialTheme.colorScheme.surface
+ fun originalOrderingWhenTheDefaultElevationIsUsed() {
+ rule.setContent {
Box(
Modifier
- .size(10.dp, 10.dp)
+ .size(10.toDp())
.semantics(mergeDescendants = true) {}
.testTag("box")
) {
Surface(
- color = surfaceColor,
- tonalElevation = 0.dp,
- selected = false,
- onClick = {}
+ onClick = {},
+ shape = RectangleShape,
+ color = Color.Yellow
) {
- absoluteTonalElevation = LocalAbsoluteTonalElevation.current
+ Box(Modifier.fillMaxSize())
+ }
+ Surface(
+ onClick = {},
+ shape = RectangleShape,
+ color = Color.Green
+ ) {
Box(Modifier.fillMaxSize())
}
}
}
- rule.runOnIdle {
- Truth.assertThat(absoluteTonalElevation).isEqualTo(0.dp)
- }
-
- rule.onNodeWithTag("box")
- .captureToImage()
- .assertShape(
- density = rule.density,
- shape = RectangleShape,
- shapeColor = surfaceColor,
- backgroundColor = Color.White
- )
- }
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
- @Test
- fun tonalElevationColorIsSetOnElevatedSurfaceColor() {
- var absoluteTonalElevation: Dp = 0.dp
- var surfaceTonalColor: Color = Color.Unspecified
- var surfaceColor: Color
- rule.setMaterialContent(lightColorScheme()) {
- surfaceColor = MaterialTheme.colorScheme.surface
- Box(
- Modifier
- .size(10.dp, 10.dp)
- .semantics(mergeDescendants = true) {}
- .testTag("box")
- ) {
- Surface(
- color = surfaceColor,
- tonalElevation = 2.dp,
- selected = false,
- onClick = {}
- ) {
- absoluteTonalElevation = LocalAbsoluteTonalElevation.current
- Box(Modifier.fillMaxSize())
- }
- surfaceTonalColor =
- MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteTonalElevation)
- }
- }
-
- rule.runOnIdle {
- Truth.assertThat(absoluteTonalElevation).isEqualTo(2.dp)
- }
-
- rule.onNodeWithTag("box")
- .captureToImage()
- .assertShape(
- density = rule.density,
- shape = RectangleShape,
- shapeColor = surfaceTonalColor,
- backgroundColor = Color.White
- )
- }
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
- @Test
- fun tonalElevationColorIsNotSetOnNonSurfaceColor() {
- var absoluteTonalElevation: Dp = 0.dp
- rule.setMaterialContent(lightColorScheme()) {
- Box(
- Modifier
- .size(10.dp, 10.dp)
- .semantics(mergeDescendants = true) {}
- .testTag("box")
- ) {
- Surface(
- color = Color.Green,
- tonalElevation = 2.dp,
- selected = false,
- onClick = {}
- ) {
- Box(Modifier.fillMaxSize())
- absoluteTonalElevation = LocalAbsoluteTonalElevation.current
- }
- }
- }
-
- rule.runOnIdle {
- Truth.assertThat(absoluteTonalElevation).isEqualTo(2.dp)
- }
-
- rule.onNodeWithTag("box")
- .captureToImage()
- .assertShape(
- density = rule.density,
- shape = RectangleShape,
- shapeColor = Color.Green,
- backgroundColor = Color.White
- )
+ rule.onNodeWithTag("box").captureToImage().assertShape(
+ density = rule.density,
+ shape = RectangleShape,
+ shapeColor = Color.Green,
+ backgroundColor = Color.White
+ )
}
@Test
fun absoluteElevationCompositionLocalIsSet() {
var outerElevation: Dp? = null
var innerElevation: Dp? = null
- rule.setMaterialContent(lightColorScheme()) {
- Surface(
- tonalElevation = 2.dp,
- selected = false,
- onClick = {}
- ) {
+ rule.setContent {
+ Surface(onClick = {}, tonalElevation = 2.toDp()) {
outerElevation = LocalAbsoluteTonalElevation.current
- Surface(
- tonalElevation = 4.dp,
- selected = false,
- onClick = {}
- ) {
+ Surface(onClick = {}, tonalElevation = 4.toDp()) {
innerElevation = LocalAbsoluteTonalElevation.current
}
}
}
rule.runOnIdle {
- Truth.assertThat(outerElevation).isEqualTo(2.dp)
- Truth.assertThat(innerElevation).isEqualTo(6.dp)
- }
- }
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
- @Test
- fun absoluteElevationIsNotUsedForShadows() {
- rule.setMaterialContent(lightColorScheme()) {
- Column {
- Box(
- Modifier
- .padding(10.dp)
- .size(10.dp, 10.dp)
- .semantics(mergeDescendants = true) {}
- .testTag("top level")
- ) {
- Surface(
- modifier = Modifier
- .fillMaxSize()
- .padding(0.dp),
- tonalElevation = 2.dp,
- shadowElevation = 2.dp,
- color = Color.Blue,
- content = {},
- selected = false,
- onClick = {}
- )
- }
-
- // Set LocalAbsoluteTonalElevation to increase the absolute elevation
- CompositionLocalProvider(
- LocalAbsoluteTonalElevation provides 2.dp
- ) {
- Box(
- Modifier
- .padding(10.dp)
- .size(10.dp, 10.dp)
- .semantics(mergeDescendants = true) {}
- .testTag("nested")
- ) {
- Surface(
- modifier = Modifier
- .fillMaxSize()
- .padding(0.dp),
- tonalElevation = 0.dp,
- shadowElevation = 2.dp,
- color = Color.Blue,
- content = {},
- selected = false,
- onClick = {}
- )
- }
- }
+ innerElevation?.let { nnInnerElevation ->
+ assertFloatPrecision(nnInnerElevation.value, 6.toDp().value)
}
- }
-
- val topLevelSurfaceBitmap = rule.onNodeWithTag("top level").captureToImage()
- val nestedSurfaceBitmap = rule.onNodeWithTag("nested").captureToImage()
- .asAndroidBitmap()
-
- topLevelSurfaceBitmap.assertPixels {
- Color(nestedSurfaceBitmap.getPixel(it.x, it.y))
+ outerElevation?.let { nnOuterElevation ->
+ assertFloatPrecision(nnOuterElevation.value, 2.toDp().value)
+ }
}
}
/**
- * Tests that composed modifiers applied to Surface are applied within the changes to
+ * Tests that composed modifiers applied to TvSurface are applied within the changes to
* [LocalContentColor], so they can consume the updated values.
*/
@Test
fun contentColorSetBeforeModifier() {
var contentColor: Color = Color.Unspecified
val expectedColor = Color.Blue
- rule.setMaterialContent(lightColorScheme()) {
+ rule.setContent {
CompositionLocalProvider(LocalContentColor provides Color.Red) {
Surface(
modifier = Modifier.composed {
contentColor = LocalContentColor.current
Modifier
},
- tonalElevation = 2.dp,
- contentColor = expectedColor,
- content = {},
- selected = false,
- onClick = {}
- )
+ onClick = {},
+ tonalElevation = 2.toDp(),
+ contentColor = expectedColor
+ ) {}
}
}
@@ -308,145 +175,217 @@
}
@Test
- fun surface_blockClicksBehind() {
- val state = mutableStateOf(0)
+ fun tvClickableOverload_semantics() {
+ val count = mutableStateOf(0)
+ rule.setContent {
+ Surface(
+ modifier = Modifier
+ .testTag("tvSurface"),
+ onClick = { count.value += 1 }
+ ) {
+ Text("${count.value}")
+ Spacer(Modifier.size(30.toDp()))
+ }
+ }
+ rule.onNodeWithTag("tvSurface")
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .assertHasClickAction()
+ .assertIsEnabled()
+ // since we merge descendants we should have text on the same node
+ .assertTextEquals("0")
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ .assertTextEquals("1")
+ }
+
+ @Test
+ fun tvClickableOverload_customSemantics() {
+ val count = mutableStateOf(0)
+ rule.setContent {
+ Surface(
+ modifier = Modifier
+ .testTag("tvSurface"),
+ onClick = { count.value += 1 },
+ role = Role.Checkbox
+ ) {
+ Text("${count.value}")
+ Spacer(Modifier.size(30.toDp()))
+ }
+ }
+ rule.onNodeWithTag("tvSurface")
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .assertHasClickAction()
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Checkbox))
+ .assertIsEnabled()
+ // since we merge descendants we should have text on the same node
+ .assertTextEquals("0")
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ .assertTextEquals("1")
+ }
+
+ @Test
+ fun tvClickableOverload_clickAction() {
+ val count = mutableStateOf(0)
+
+ rule.setContent {
+ Surface(
+ modifier = Modifier
+ .testTag("tvSurface"),
+ onClick = { count.value += 1 }
+ ) {
+ Spacer(modifier = Modifier.size(30.toDp()))
+ }
+ }
+ rule.onNodeWithTag("tvSurface")
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ Truth.assertThat(count.value).isEqualTo(1)
+
+ rule.onNodeWithTag("tvSurface").performKeyInput { pressKey(Key.DirectionCenter) }
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ Truth.assertThat(count.value).isEqualTo(3)
+ }
+
+ @Test
+ fun tvSurface_onDisable_clickFails() {
+ val count = mutableStateOf(0f)
+ val enabled = mutableStateOf(true)
+
+ rule.setContent {
+ Surface(
+ modifier = Modifier
+ .testTag("tvSurface"),
+ onClick = { count.value += 1 },
+ enabled = enabled.value
+ ) {
+ Spacer(Modifier.size(30.toDp()))
+ }
+ }
+ rule.onNodeWithTag("tvSurface")
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .assertIsEnabled()
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+
+ Truth.assertThat(count.value).isEqualTo(1)
+ rule.runOnIdle {
+ enabled.value = false
+ }
+
+ rule.onNodeWithTag("tvSurface")
+ .assertIsNotEnabled()
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ Truth.assertThat(count.value).isEqualTo(1)
+ }
+
+ @Test
+ fun tvClickableOverload_interactionSource() {
+ val interactionSource = MutableInteractionSource()
+
+ lateinit var scope: CoroutineScope
+
+ rule.setContent {
+ scope = rememberCoroutineScope()
+ Surface(
+ modifier = Modifier
+ .testTag("tvSurface"),
+ onClick = {},
+ interactionSource = interactionSource
+ ) {
+ Spacer(Modifier.size(30.toDp()))
+ }
+ }
+
+ val interactions = mutableListOf<Interaction>()
+
+ scope.launch {
+ interactionSource.interactions.collect { interactions.add(it) }
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(interactions).isEmpty()
+ }
+
+ rule.onNodeWithTag("tvSurface")
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .performKeyInput { keyDown(Key.DirectionCenter) }
+
+ rule.runOnIdle {
+ Truth.assertThat(interactions).hasSize(2)
+ Truth.assertThat(interactions[1]).isInstanceOf(PressInteraction.Press::class.java)
+ }
+
+ rule.onNodeWithTag("tvSurface").performKeyInput { keyUp(Key.DirectionCenter) }
+
+ rule.runOnIdle {
+ Truth.assertThat(interactions).hasSize(3)
+ Truth.assertThat(interactions.first()).isInstanceOf(FocusInteraction.Focus::class.java)
+ Truth.assertThat(interactions[1]).isInstanceOf(PressInteraction.Press::class.java)
+ Truth.assertThat(interactions[2]).isInstanceOf(PressInteraction.Release::class.java)
+ Truth.assertThat((interactions[2] as PressInteraction.Release).press)
+ .isEqualTo(interactions[1])
+ }
+ }
+
+ @Test
+ fun tvSurface_allowsFinalPassChildren() {
+ val hitTested = mutableStateOf(false)
+
rule.setContent {
Box(Modifier.fillMaxSize()) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .testTag("clickable")
- .clickable { state.value += 1 },
- ) { Text("button fullscreen") }
Surface(
modifier = Modifier
.fillMaxSize()
- .testTag("surface"),
- onClick = {},
- selected = false
- ) {}
+ .testTag("tvSurface"),
+ onClick = {}
+ ) {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("pressable")
+ .pointerInput(Unit) {
+ awaitEachGesture {
+ hitTested.value = true
+ val event = awaitPointerEvent(PointerEventPass.Final)
+ Truth
+ .assertThat(event.changes[0].isConsumed)
+ .isFalse()
+ }
+ }
+ )
+ }
}
}
- rule.onNodeWithTag("clickable").assertHasClickAction().performClick()
- // still 0
- Truth.assertThat(state.value).isEqualTo(0)
+ rule.onNodeWithTag("tvSurface").performSemanticsAction(SemanticsActions.RequestFocus)
+ rule.onNodeWithTag("pressable", true)
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ Truth.assertThat(hitTested.value).isTrue()
}
+ @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
@Test
- fun selectable_semantics() {
- val selected = mutableStateOf(false)
- rule.setMaterialContent(lightColorScheme()) {
- Surface(
- selected = selected.value,
- onClick = { selected.value = !selected.value },
- modifier = Modifier.testTag("surface"),
- ) {
- Text("${selected.value}")
- Spacer(Modifier.size(30.dp))
- }
- }
- rule.onNodeWithTag("surface")
- .assertHasClickAction()
- .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Tab))
- .assertIsEnabled()
- // since we merge descendants we should have text on the same node
- .assertTextEquals("false")
- .performClick()
- .assertTextEquals("true")
- }
+ fun tvSurface_reactsToStateChange() {
+ val interactionSource = MutableInteractionSource()
+ var isPressed by mutableStateOf(false)
- @Test
- fun selectable_customSemantics() {
- val selected = mutableStateOf(false)
- rule.setMaterialContent(lightColorScheme()) {
+ rule.setContent {
+ isPressed = interactionSource.collectIsPressedAsState().value
Surface(
- selected = selected.value,
- onClick = { selected.value = !selected.value },
modifier = Modifier
- .semantics { role = Role.Switch }
- .testTag("surface"),
- ) {
- Text("${selected.value}")
- Spacer(Modifier.size(30.dp))
- }
+ .testTag("tvSurface")
+ .size(100.toDp()),
+ onClick = {},
+ interactionSource = interactionSource
+ ) {}
}
- rule.onNodeWithTag("surface")
- .assertHasClickAction()
- .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Switch))
- .assertIsEnabled()
- // since we merge descendants we should have text on the same node
- .assertTextEquals("false")
- .performClick()
- .assertTextEquals("true")
- }
- @Test
- fun selectable_clickAction() {
- val selected = mutableStateOf(false)
- rule.setMaterialContent(lightColorScheme()) {
- Surface(
- selected = selected.value,
- onClick = { selected.value = !selected.value },
- modifier = Modifier.testTag("surface")
- ) { Spacer(Modifier.size(30.dp)) }
+ with(rule.onNodeWithTag("tvSurface")) {
+ performSemanticsAction(SemanticsActions.RequestFocus)
+ assertIsFocused()
+ performKeyInput { keyDown(Key.DirectionCenter) }
}
- rule.onNodeWithTag("surface").performClick()
- Truth.assertThat(selected.value).isTrue()
- rule.onNodeWithTag("surface").performClick()
- Truth.assertThat(selected.value).isFalse()
- }
+ rule.waitUntil(condition = { isPressed })
- @Test
- fun selectable_clickOutsideShapeBounds() {
- val selected = mutableStateOf(false)
- rule.setMaterialContent(lightColorScheme()) {
- Surface(
- selected = selected.value,
- onClick = { selected.value = !selected.value },
- modifier = Modifier.testTag("surface"),
- shape = CircleShape
- ) { Spacer(Modifier.size(100.dp)) }
- }
- // Click inside the circular shape bounds. Expecting a selection change.
- rule.onNodeWithTag("surface").performClick()
- Truth.assertThat(selected.value).isTrue()
-
- // Click outside the circular shape bounds. Expecting a selection to stay as it.
- rule.onNodeWithTag("surface").performTouchInput { click(Offset(10f, 10f)) }
- Truth.assertThat(selected.value).isTrue()
- }
-
- @Test
- fun selectable_smallTouchTarget_clickOutsideShapeBounds() {
- val selected = mutableStateOf(false)
- rule.setMaterialContent(lightColorScheme()) {
- Surface(
- selected = selected.value,
- onClick = { selected.value = !selected.value },
- modifier = Modifier.testTag("surface"),
- shape = CircleShape
- ) { Spacer(Modifier.size(40.dp)) }
- }
- // Click inside the circular shape bounds. Expecting a selection change.
- rule.onNodeWithTag("surface").performClick()
- Truth.assertThat(selected.value).isTrue()
-
- // Click outside the circular shape bounds. Still expecting a selection change, as the
- // touch target has a minimum size of 48dp.
- rule.onNodeWithTag("surface").performTouchInput { click(Offset(2f, 2f)) }
- Truth.assertThat(selected.value).isFalse()
- }
-
- private fun ComposeContentTestRule.setMaterialContent(
- colorScheme: ColorScheme = lightColorScheme(),
- content: @Composable () -> Unit
- ) {
- setContent {
- MaterialTheme(
- colorScheme = colorScheme,
- content = content
- )
- }
+ Truth.assertThat(isPressed).isTrue()
}
}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
index 67aa138..36e4df2 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
@@ -14,43 +14,55 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalTvMaterial3Api::class)
-
package androidx.tv.material3
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.selection.selectable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
/**
* Material surface is the central metaphor in material design. Each surface exists at a given
* elevation, which influences how that piece of surface visually relates to other surfaces and how
* that surface is modified by tonal variance.
*
- * This version of Surface is responsible for a selection handling as well as everything else that
- * a regular Surface does:
+ * This version of Surface is responsible for a click handling as well as everything else that a
+ * regular Surface does:
*
- * This selectable Surface is responsible for:
+ * This clickable Surface is responsible for:
*
* 1) Clipping: Surface clips its children to the shape specified by [shape]
*
@@ -71,16 +83,18 @@
* this Surface.
*
* 5) Click handling. This version of surface will react to the clicks, calling [onClick] lambda,
- * and updating the [interactionSource] when [PressInteraction] occurs.
+ * updating the [interactionSource] when [PressInteraction] occurs, and showing ripple indication in
+ * response to press events. If you don't need click handling, consider using the Surface function
+ * that doesn't require [onClick] param. If you need to set a custom label for the [onClick], apply
+ * a `Modifier.semantics { onClick(label = "YOUR_LABEL", action = null) }` to the Surface.
*
- * 6) Semantics for selection. Just like with [Modifier.selectable], selectable version of Surface
- * will produce semantics to indicate that it is selected. Also, by default, accessibility services
- * will describe the element as [Role.Tab]. You may change this by passing a desired [Role] with a
+ * 6) Semantics for clicks. Just like with [Modifier.clickable], clickable version of Surface will
+ * produce semantics to indicate that it is clicked. Also, by default, accessibility services will
+ * describe the element as [Role.Button]. You may change this by passing a desired [Role] with a
* [Modifier.semantics].
*
* To manually retrieve the content color inside a surface, use [LocalContentColor].
*
- * @param selected whether or not this Surface is selected
* @param onClick callback to be called when the surface is clicked
* @param modifier Modifier to be applied to the layout corresponding to the surface
* @param enabled Controls the enabled state of the surface. When `false`, this surface will not be
@@ -94,29 +108,60 @@
* @param border Optional border to draw on top of the surface
* @param tonalElevation When [color] is [ColorScheme.surface], a higher the elevation will result
* in a darker color in light theme and lighter color in dark theme.
+ * @param role The type of user interface element. Accessibility services might use this
+ * to describe the element or do customizations
* @param shadowElevation The size of the shadow below the surface. Note that It will not affect z
* index of the Surface. If you want to change the drawing order you can use `Modifier.zIndex`.
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this Surface. You can create and pass in your own remembered [MutableInteractionSource] if
* you want to observe [Interaction]s and customize the appearance / behavior of this Surface in
* different [Interaction]s.
- * @param content the composable content to be displayed inside the surface
+ * @param content The content inside this Surface
*/
@ExperimentalTvMaterial3Api
-@Composable
@NonRestartableComposable
+@Composable
fun Surface(
- selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
shape: Shape = RectangleShape,
color: Color = MaterialTheme.colorScheme.surface,
contentColor: Color = contentColorFor(color),
+ border: BorderStroke? = null,
+ tonalElevation: Dp = 0.dp,
+ role: Role? = null,
+ shadowElevation: Dp = 0.dp,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ content: @Composable () -> Unit
+) {
+ SurfaceImpl(
+ modifier = modifier.tvClickable(
+ enabled = enabled,
+ onClick = onClick,
+ interactionSource = interactionSource,
+ role = role
+ ),
+ shape = shape,
+ color = color,
+ contentColor = contentColor,
+ tonalElevation = tonalElevation,
+ shadowElevation = shadowElevation,
+ border = border,
+ content = content
+ )
+}
+
+@ExperimentalTvMaterial3Api
+@Composable
+private fun SurfaceImpl(
+ modifier: Modifier = Modifier,
+ shape: Shape = RectangleShape,
+ color: Color = MaterialTheme.colorScheme.surface,
+ contentColor: Color = contentColorFor(color),
tonalElevation: Dp = 0.dp,
shadowElevation: Dp = 0.dp,
border: BorderStroke? = null,
- interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable () -> Unit
) {
val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
@@ -134,14 +179,6 @@
),
border = border,
shadowElevation = shadowElevation
- )
- .selectable(
- selected = selected,
- interactionSource = interactionSource,
- indication = null,
- enabled = enabled,
- role = Role.Tab,
- onClick = onClick
),
propagateMinConstraints = true
) {
@@ -155,12 +192,106 @@
backgroundColor: Color,
border: BorderStroke?,
shadowElevation: Dp
-) = this.shadow(shadowElevation, shape, clip = false)
+) = this
+ .shadow(shadowElevation, shape, clip = false)
.then(if (border != null) Modifier.border(border, shape) else Modifier)
.background(color = backgroundColor, shape = shape)
.clip(shape)
+/**
+ * This modifier handles click, press, and focus events for a TV composable.
+ * @param enabled decides whether [onClick] or [onValueChanged] is executed
+ * @param onClick executes the provided lambda
+ * @param value differentiates whether the current item is selected or unselected
+ * @param onValueChanged executes the provided lambda while returning the inverse state of [value]
+ * @param interactionSource used to emit [PressInteraction] events
+ * @param role used to define this composable's semantic role (for Accessibility purposes)
+ */
+private fun Modifier.tvClickable(
+ enabled: Boolean,
+ onClick: (() -> Unit)? = null,
+ value: Boolean = false,
+ onValueChanged: ((Boolean) -> Unit)? = null,
+ interactionSource: MutableInteractionSource,
+ role: Role?
+) = this
+ .handleDPadEnter(
+ enabled = enabled,
+ interactionSource = interactionSource,
+ onClick = onClick,
+ value = value,
+ onValueChanged = onValueChanged
+ )
+ .focusable(interactionSource = interactionSource)
+ .semantics(mergeDescendants = true) {
+ onClick {
+ onClick?.let { nnOnClick ->
+ nnOnClick()
+ return@onClick true
+ } ?: onValueChanged?.let { nnOnValueChanged ->
+ nnOnValueChanged(!value)
+ return@onClick true
+ }
+ false
+ }
+ role?.let { nnRole -> this.role = nnRole }
+ if (!enabled) {
+ disabled()
+ }
+ }
+
+private fun Modifier.handleDPadEnter(
+ enabled: Boolean,
+ interactionSource: MutableInteractionSource,
+ onClick: (() -> Unit)?,
+ value: Boolean,
+ onValueChanged: ((Boolean) -> Unit)?
+) = composed(
+ inspectorInfo = debugInspectorInfo {
+ name = "handleDPadEnter"
+ properties["enabled"] = enabled
+ properties["interactionSource"] = interactionSource
+ properties["onClick"] = onClick
+ properties["onValueChanged"] = onValueChanged
+ properties["value"] = value
+ }
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val pressInteraction = remember { PressInteraction.Press(Offset.Zero) }
+ var isPressed by remember { mutableStateOf(false) }
+ this.then(
+ onKeyEvent { keyEvent ->
+ if (AcceptableKeys.any { keyEvent.nativeKeyEvent.keyCode == it } && enabled) {
+ when (keyEvent.nativeKeyEvent.action) {
+ NativeKeyEvent.ACTION_DOWN -> {
+ if (!isPressed) {
+ isPressed = true
+ coroutineScope.launch {
+ interactionSource.emit(pressInteraction)
+ }
+ }
+ }
+
+ NativeKeyEvent.ACTION_UP -> {
+ if (isPressed) {
+ isPressed = false
+ coroutineScope.launch {
+ interactionSource.emit(PressInteraction.Release(pressInteraction))
+ }
+ onClick?.invoke()
+ onValueChanged?.invoke(!value)
+ }
+ }
+ }
+ return@onKeyEvent KeyEventPropagation.StopPropagation
+ }
+ KeyEventPropagation.ContinuePropagation
+ }
+ )
+}
+
@Composable
+@ExperimentalTvMaterial3Api
private fun surfaceColorAtElevation(color: Color, elevation: Dp): Color {
return if (color == MaterialTheme.colorScheme.surface) {
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
@@ -170,8 +301,13 @@
}
/**
- * CompositionLocal containing the current absolute elevation provided by [Surface] components. This
+ * CompositionLocal containing the current absolute elevation provided by Surface components. This
* absolute elevation is a sum of all the previous elevations. Absolute elevation is only used for
- * calculating surface tonal colors, and is *not* used for drawing the shadow in a [Surface].
+ * calculating surface tonal colors, and is *not* used for drawing the shadow in a [SurfaceImpl].
*/
val LocalAbsoluteTonalElevation = compositionLocalOf { 0.dp }
+private val AcceptableKeys = listOf(
+ NativeKeyEvent.KEYCODE_DPAD_CENTER,
+ NativeKeyEvent.KEYCODE_ENTER,
+ NativeKeyEvent.KEYCODE_NUMPAD_ENTER
+)
diff --git a/viewpager2/OWNERS b/viewpager2/OWNERS
index 6ee1c5c..859a7b9 100644
--- a/viewpager2/OWNERS
+++ b/viewpager2/OWNERS
@@ -1,2 +1,2 @@
-# Bug component: 561920
+# Bug component: 607924
jgielzak@google.com
\ No newline at end of file
diff --git a/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt
index b07f84b3..bee6208 100644
--- a/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt
+++ b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt
@@ -16,6 +16,7 @@
package androidx.wear.compose.foundation.lazy
+import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
@@ -24,6 +25,7 @@
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
@@ -1000,6 +1002,20 @@
rule.waitForIdle()
assertThat(recompositionCount).isEqualTo(2)
}
+
+ @Test
+ fun scalingLazyColumnCanBeNestedOnHorizontalScrollingComponent() {
+ rule.setContent {
+ val horizontalScrollState = rememberScrollState()
+ Box(
+ modifier = Modifier.horizontalScroll(horizontalScrollState),
+ ) {
+ ScalingLazyColumn {
+ item { Box(Modifier.size(10.dp)) }
+ }
+ }
+ }
+ }
}
internal const val TestTouchSlop = 18f
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumn.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumn.kt
index a03e3d5..ac02f9e 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumn.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumn.kt
@@ -723,7 +723,6 @@
public fun Modifier.verticalNegativePadding(
extraPadding: Dp,
) = layout { measurable, constraints ->
- require(constraints.hasBoundedWidth)
require(constraints.hasBoundedHeight)
val topAndBottomPadding = (extraPadding * 2).roundToPx()
val placeable = measurable.measure(
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index 029a1e9..445b1fc 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -562,8 +562,10 @@
}
public final class StepperKt {
- method @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, optional boolean enableRangeSemantics, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, optional boolean enableRangeSemantics, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<? extends kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<? extends kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<? extends kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<? extends kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit> content);
}
public final class SwipeToDismissBoxDefaults {
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index 244f8e6..d734539 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -610,8 +610,10 @@
}
public final class StepperKt {
- method @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, optional boolean enableRangeSemantics, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, optional boolean enableRangeSemantics, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<? extends kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<? extends kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<? extends kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<? extends kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit> content);
}
@androidx.compose.runtime.Immutable @androidx.wear.compose.material.ExperimentalWearMaterialApi public final class SwipeProgress<T> {
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index 029a1e9..445b1fc 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -562,8 +562,10 @@
}
public final class StepperKt {
- method @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, optional boolean enableRangeSemantics, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, optional boolean enableRangeSemantics, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void Stepper(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,? extends kotlin.Unit> onValueChange, int steps, kotlin.jvm.functions.Function0<? extends kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<? extends kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<? extends kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<? extends kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,? extends kotlin.Unit> content);
}
public final class SwipeToDismissBoxDefaults {
diff --git a/wear/compose/compose-material/samples/build.gradle b/wear/compose/compose-material/samples/build.gradle
index 0e71692a..fc8c8f3 100644
--- a/wear/compose/compose-material/samples/build.gradle
+++ b/wear/compose/compose-material/samples/build.gradle
@@ -33,6 +33,7 @@
implementation(project(":compose:foundation:foundation-layout"))
implementation(project(":compose:runtime:runtime"))
implementation(project(":compose:ui:ui"))
+ implementation(project(":compose:ui:ui-util"))
implementation(project(":compose:ui:ui-text"))
implementation(project(":compose:material:material-icons-core"))
implementation(project(":wear:compose:compose-material"))
diff --git a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/StepperSample.kt b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/StepperSample.kt
index b87f558..ca2344c 100644
--- a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/StepperSample.kt
+++ b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/StepperSample.kt
@@ -17,15 +17,22 @@
package androidx.wear.compose.material.samples
import androidx.annotation.Sampled
+import androidx.compose.foundation.progressSemantics
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.setProgress
+import androidx.compose.ui.util.lerp
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.Stepper
import androidx.wear.compose.material.StepperDefaults
import androidx.wear.compose.material.Text
+import kotlin.math.roundToInt
@Sampled
@Composable
@@ -53,3 +60,70 @@
valueProgression = 1..10
) { Text("Value: $value") }
}
+
+@Sampled
+@Composable
+fun StepperWithoutRangeSemanticsSample() {
+ var value by remember { mutableStateOf(2f) }
+ Stepper(
+ value = value,
+ onValueChange = { value = it },
+ valueRange = 1f..4f,
+ increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
+ steps = 7,
+ enableRangeSemantics = false
+ ) { Text("Value: $value") }
+}
+
+@Sampled
+@Composable
+fun StepperWithCustomSemanticsSample() {
+ var value by remember { mutableStateOf(2f) }
+ val valueRange = 1f..4f
+ val onValueChange = { i: Float -> value = i }
+ val steps = 7
+
+ Stepper(
+ value = value,
+ onValueChange = onValueChange,
+ valueRange = valueRange,
+ modifier = Modifier.customSemantics(value, true, onValueChange, valueRange, steps),
+ increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
+ steps = steps,
+ enableRangeSemantics = false
+ ) { Text("Value: $value") }
+}
+
+// Declaring the custom semantics for StepperWithCustomSemanticsSample
+private fun Modifier.customSemantics(
+ value: Float,
+ enabled: Boolean,
+ onValueChange: (Float) -> Unit,
+ valueRange: ClosedFloatingPointRange<Float>,
+ steps: Int
+): Modifier = semantics(mergeDescendants = true) {
+
+ if (!enabled) disabled()
+ setProgress(
+ action = { targetValue ->
+ val newStepIndex = ((value - valueRange.start) /
+ (valueRange.endInclusive - valueRange.start) * (steps + 1))
+ .roundToInt().coerceIn(0, steps + 1)
+
+ if (value.toInt() == newStepIndex) {
+ false
+ } else {
+ onValueChange(targetValue)
+ true
+ }
+ }
+ )
+}.progressSemantics(
+ lerp(
+ valueRange.start, valueRange.endInclusive,
+ value / (steps + 1).toFloat()
+ ).coerceIn(valueRange),
+ valueRange, steps
+)
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/StepperTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/StepperTest.kt
index 5e1b405..d8a4b41 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/StepperTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/StepperTest.kt
@@ -351,6 +351,57 @@
.assertContentDescriptionContains(testContentDescription)
}
+ @Test
+ fun supports_stepper_range_semantics_by_default() {
+ val value = 1f
+ val steps = 5
+ val valueRange = 0f..(steps + 1).toFloat()
+
+ val modifier = Modifier.testTag(TEST_TAG)
+
+ rule.setContentWithTheme {
+ Stepper(
+ modifier = modifier,
+ value = value,
+ steps = steps,
+ valueRange = valueRange,
+ onValueChange = { },
+ increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
+ ) {}
+ }
+ rule.waitForIdle()
+
+ rule.onNodeWithTag(TEST_TAG, true)
+ .assertExists()
+ .assertRangeInfoEquals(ProgressBarRangeInfo(value, valueRange, steps))
+ }
+
+ @Test(expected = java.lang.AssertionError::class)
+ fun disable_stepper_semantics_with_enableDefaultSemantics_false() {
+ val value = 1f
+ val steps = 5
+ val valueRange = 0f..(steps + 1).toFloat()
+
+ rule.setContentWithTheme {
+ Stepper(
+ modifier = Modifier.testTag(TEST_TAG),
+ value = value,
+ steps = steps,
+ valueRange = valueRange,
+ onValueChange = { },
+ increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
+ enableRangeSemantics = false
+ ) {}
+ }
+ rule.waitForIdle()
+ // Should throw assertion error for assertRangeInfoEquals
+ rule.onNodeWithTag(TEST_TAG, true)
+ .assertExists()
+ .assertRangeInfoEquals(ProgressBarRangeInfo(value, valueRange, steps))
+ }
+
private fun verifyDisabledColors(increase: Boolean, value: Float) {
val state = mutableStateOf(value)
var expectedIconColor = Color.Transparent
@@ -469,6 +520,63 @@
newValue = 5,
expectedFinalValue = 6
)
+
+ @Test
+ fun supports_stepper_range_semantics_by_default() {
+ val value = 1
+ val valueProgression = 0..10
+
+ rule.setContentWithTheme {
+ Stepper(
+ value = value,
+ onValueChange = {},
+ valueProgression = valueProgression,
+ increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {}
+ }
+ rule.waitForIdle()
+ // Should throw assertion error for assertRangeInfoEquals
+ rule.onNodeWithTag(TEST_TAG, true)
+ .assertExists()
+ .assertRangeInfoEquals(
+ ProgressBarRangeInfo(
+ value.toFloat(),
+ valueProgression.first.toFloat()..valueProgression.last.toFloat(),
+ valueProgression.stepsNumber()
+ )
+ )
+ }
+
+ @Test(expected = java.lang.AssertionError::class)
+ fun disable_stepper_semantics_with_enableDefaultSemantics_false() {
+ val value = 1
+ val valueProgression = 0..10
+
+ rule.setContentWithTheme {
+ Stepper(
+ value = value,
+ onValueChange = {},
+ valueProgression = valueProgression,
+ increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
+ modifier = Modifier.testTag(TEST_TAG),
+ enableRangeSemantics = false
+ ) {}
+ }
+ rule.waitForIdle()
+ // Should throw assertion error for assertRangeInfoEquals
+ rule.onNodeWithTag(TEST_TAG, true)
+ .assertExists()
+ .assertRangeInfoEquals(
+ ProgressBarRangeInfo(
+ value.toFloat(),
+ valueProgression.first.toFloat()..valueProgression.last.toFloat(),
+ valueProgression.stepsNumber()
+ )
+ )
+ }
}
private fun ComposeContentTestRule.setNewValueAndCheck(
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Stepper.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Stepper.kt
index 44b5fe7..ac33051 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Stepper.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Stepper.kt
@@ -60,6 +60,8 @@
* not be triggered.
*
* @sample androidx.wear.compose.material.samples.StepperSample
+ * @sample androidx.wear.compose.material.samples.StepperWithoutRangeSemanticsSample
+ * @sample androidx.wear.compose.material.samples.StepperWithCustomSemanticsSample
*
* @param value Current value of the Stepper. If outside of [valueRange] provided, value will be
* coerced to this range.
@@ -76,9 +78,13 @@
* @param contentColor [Color] representing the color for [content] in the middle.
* @param iconColor Icon tint [Color] which used by [increaseIcon] and [decreaseIcon]
* that defaults to [contentColor], unless specifically overridden.
+ * @param enableRangeSemantics Boolean to decide if range semantics should be enabled.
+ * Set to false to disable default stepper range semantics. Alternatively to customize semantics
+ * set this value as false and chain new semantics to the modifier.
+ * @param content Content body for the Stepper.
*/
@Composable
-public fun Stepper(
+fun Stepper(
value: Float,
onValueChange: (Float) -> Unit,
steps: Int,
@@ -89,6 +95,7 @@
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
iconColor: Color = contentColor,
+ enableRangeSemantics: Boolean = true,
content: @Composable BoxScope.() -> Unit
) {
require(steps >= 0) { "steps should be >= 0" }
@@ -101,17 +108,15 @@
}
Column(
- modifier = modifier
- .fillMaxSize()
- .background(backgroundColor)
- .rangeSemantics(
- currentStep,
- true,
- onValueChange,
- valueRange,
- steps
- ),
- verticalArrangement = Arrangement.spacedBy(8.dp)
+ modifier = modifier.fillMaxSize().background(backgroundColor).then(
+ if (enableRangeSemantics) {
+ Modifier.rangeSemantics(
+ currentStep, true, onValueChange, valueRange, steps
+ )
+ } else {
+ Modifier
+ }
+ ), verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Increase button.
FullScreenButton(
@@ -123,9 +128,7 @@
content = increaseIcon
)
Box(
- modifier = Modifier
- .fillMaxWidth()
- .weight(StepperDefaults.ContentWeight),
+ modifier = Modifier.fillMaxWidth().weight(StepperDefaults.ContentWeight),
contentAlignment = Alignment.Center,
) {
CompositionLocalProvider(
@@ -182,9 +185,13 @@
* @param contentColor [Color] representing the color for [content] in the middle.
* @param iconColor Icon tint [Color] which used by [increaseIcon] and [decreaseIcon]
* that defaults to [contentColor], unless specifically overridden.
+ * @param enableRangeSemantics Boolean to decide if default stepper semantics should be enabled.
+ * Set to false to disable default stepper range semantics. Alternatively to customize semantics
+ * set this value as false and chain new semantics to the modifier.
+ * @param content Content body for the Stepper.
*/
@Composable
-public fun Stepper(
+fun Stepper(
value: Int,
onValueChange: (Int) -> Unit,
valueProgression: IntProgression,
@@ -194,6 +201,7 @@
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
iconColor: Color = contentColor,
+ enableRangeSemantics: Boolean = true,
content: @Composable BoxScope.() -> Unit
) {
Stepper(
@@ -207,6 +215,134 @@
backgroundColor = backgroundColor,
contentColor = contentColor,
iconColor = iconColor,
+ enableRangeSemantics = enableRangeSemantics,
+ content = content
+ )
+}
+
+/**
+ * [Stepper] allows users to make a selection from a range of values.
+ * It's a full-screen control with increase button on the top, decrease button on the bottom and
+ * a slot (expected to have either [Text] or [Chip]) in the middle.
+ * Value can be increased and decreased by clicking on the increase and decrease buttons.
+ * Buttons can have custom icons - [decreaseIcon] and [increaseIcon].
+ * Step value is calculated as the difference between min and max values divided by [steps]+1.
+ * Stepper itself doesn't show the current value but can be displayed via the content slot or
+ * [PositionIndicator] if required.
+ *
+ * @sample androidx.wear.compose.material.samples.StepperSample
+ *
+ * @param value Current value of the Stepper. If outside of [valueRange] provided, value will be
+ * coerced to this range.
+ * @param onValueChange Lambda in which value should be updated
+ * @param steps Specifies the number of discrete values, excluding min and max values, evenly
+ * distributed across the whole value range. Must not be negative. If 0, stepper will have only
+ * min and max values and no steps in between
+ * @param decreaseIcon A slot for an icon which is placed on the decrease (bottom) button
+ * @param increaseIcon A slot for an icon which is placed on the increase (top) button
+ * @param modifier Modifiers for the Stepper layout
+ * @param valueRange Range of values that Stepper value can take. Passed [value] will be coerced to
+ * this range
+ * @param backgroundColor [Color] representing the background color for the stepper.
+ * @param contentColor [Color] representing the color for [content] in the middle.
+ * @param iconColor Icon tint [Color] which used by [increaseIcon] and [decreaseIcon]
+ * that defaults to [contentColor], unless specifically overridden.
+ */
+@Deprecated(
+ "This overload is provided for backwards compatibility with Compose for Wear OS 1.1. " +
+ "A newer overload is available with an additional enableDefaultSemantics parameter.",
+ level = DeprecationLevel.HIDDEN
+)
+@Composable
+public fun Stepper(
+ value: Float,
+ onValueChange: (Float) -> Unit,
+ steps: Int,
+ decreaseIcon: @Composable () -> Unit,
+ increaseIcon: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ valueRange: ClosedFloatingPointRange<Float> = 0f..(steps + 1).toFloat(),
+ backgroundColor: Color = MaterialTheme.colors.background,
+ contentColor: Color = contentColorFor(backgroundColor),
+ iconColor: Color = contentColor,
+ content: @Composable BoxScope.() -> Unit
+) = Stepper(
+ value = value,
+ onValueChange = onValueChange,
+ steps = steps,
+ decreaseIcon = decreaseIcon,
+ increaseIcon = increaseIcon,
+ modifier = modifier,
+ valueRange = valueRange,
+ backgroundColor = backgroundColor,
+ contentColor = contentColor,
+ iconColor = iconColor,
+ enableRangeSemantics = true,
+ content = content
+)
+
+/**
+ * [Stepper] allows users to make a selection from a range of values.
+ * It's a full-screen control with increase button on the top, decrease button on the bottom and
+ * a slot (expected to have either [Text] or [Chip]) in the middle.
+ * Value can be increased and decreased by clicking on the increase and decrease buttons.
+ * Buttons can have custom icons - [decreaseIcon] and [increaseIcon].
+ * Stepper itself doesn't show the current value but can be displayed via the content slot or
+ * [PositionIndicator] if required.
+ *
+ * @sample androidx.wear.compose.material.samples.StepperWithIntegerSample
+ *
+ * A number of steps is calculated as the difference between max and min values of
+ * [valueProgression] divided by [valueProgression].step - 1.
+ * For example, with a range of 100..120 and a step 5,
+ * number of steps will be (120-100)/ 5 - 1 = 3. Steps are 100(first), 105, 110, 115, 120(last)
+ *
+ * If [valueProgression] range is not equally divisible by [valueProgression].step,
+ * then [valueProgression].last will be adjusted to the closest divisible value in the range.
+ * For example, 1..13 range and a step = 5, steps will be 1(first) , 6 , 11(last)
+ *
+ * @param value Current value of the Stepper. If outside of [valueProgression] provided, value will be
+ * coerced to this range.
+ * @param onValueChange Lambda in which value should be updated
+ * @param valueProgression Progression of values that Stepper value can take. Consists of
+ * rangeStart, rangeEnd and step. Range will be equally divided by step size
+ * @param decreaseIcon A slot for an icon which is placed on the decrease (bottom) button
+ * @param increaseIcon A slot for an icon which is placed on the increase (top) button
+ * @param modifier Modifiers for the Stepper layout
+ * @param backgroundColor [Color] representing the background color for the stepper.
+ * @param contentColor [Color] representing the color for [content] in the middle.
+ * @param iconColor Icon tint [Color] which used by [increaseIcon] and [decreaseIcon]
+ * that defaults to [contentColor], unless specifically overridden.
+ */
+@Deprecated(
+ "This overload is provided for backwards compatibility with Compose for Wear OS 1.1. " +
+ "A newer overload is available with an additional enableDefaultSemantics parameter.",
+ level = DeprecationLevel.HIDDEN
+)
+@Composable
+fun Stepper(
+ value: Int,
+ onValueChange: (Int) -> Unit,
+ valueProgression: IntProgression,
+ decreaseIcon: @Composable () -> Unit,
+ increaseIcon: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ backgroundColor: Color = MaterialTheme.colors.background,
+ contentColor: Color = contentColorFor(backgroundColor),
+ iconColor: Color = contentColor,
+ content: @Composable BoxScope.() -> Unit
+) {
+ Stepper(
+ value = value,
+ onValueChange = onValueChange,
+ valueProgression = valueProgression,
+ decreaseIcon = decreaseIcon,
+ increaseIcon = increaseIcon,
+ modifier = modifier,
+ backgroundColor = backgroundColor,
+ contentColor = contentColor,
+ iconColor = iconColor,
+ enableRangeSemantics = true,
content = content
)
}
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
index c44c591..d2edf1d 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
@@ -69,7 +69,9 @@
import androidx.wear.compose.material.samples.SplitToggleChipWithCheckbox
import androidx.wear.compose.material.samples.StatefulSwipeToDismissBox
import androidx.wear.compose.material.samples.StepperSample
+import androidx.wear.compose.material.samples.StepperWithCustomSemanticsSample
import androidx.wear.compose.material.samples.StepperWithIntegerSample
+import androidx.wear.compose.material.samples.StepperWithoutRangeSemanticsSample
import androidx.wear.compose.material.samples.TextPlaceholder
import androidx.wear.compose.material.samples.TimeTextAnimation
import androidx.wear.compose.material.samples.TimeTextWithFullDateAndTimeFormat
@@ -262,6 +264,12 @@
ComposableDemo("Integer Stepper") {
Centralize { StepperWithIntegerSample() }
},
+ ComposableDemo("Stepper without RangeSemantics") {
+ Centralize { StepperWithoutRangeSemanticsSample() }
+ },
+ ComposableDemo("Stepper with customSemantics") {
+ Centralize { StepperWithCustomSemanticsSample() }
+ }
)
),
DemoCategory(
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/current.txt b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
index e6f50d0..654b5cb 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
@@ -1 +1,58 @@
// Signature format: 4.0
+package androidx.wear.protolayout.expression.pipeline {
+
+ public interface BoundDynamicType extends java.lang.AutoCloseable {
+ method @UiThread public void close();
+ }
+
+ public class DynamicTypeEvaluator implements java.lang.AutoCloseable {
+ ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore);
+ ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager);
+ method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString, android.icu.util.ULocale, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.String!>);
+ method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
+ method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Float!>);
+ method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
+ method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Boolean!>);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void close();
+ method @UiThread public void disablePlatformDataSources();
+ method @UiThread public void enablePlatformDataSources();
+ }
+
+ public interface DynamicTypeValueReceiver<T> {
+ method @UiThread public void onData(T);
+ method @UiThread public void onInvalidated();
+ }
+
+ public class ObservableStateStore {
+ ctor public ObservableStateStore(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue!>);
+ method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue!>);
+ }
+
+ public interface QuotaManager {
+ method public void releaseQuota(int);
+ method public boolean tryAcquireQuota(int);
+ }
+
+}
+
+package androidx.wear.protolayout.expression.pipeline.sensor {
+
+ public interface SensorGateway extends java.io.Closeable {
+ method public void close();
+ method @UiThread public void registerSensorGatewayConsumer(androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+ method @UiThread public void registerSensorGatewayConsumer(java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+ method @UiThread public void unregisterSensorGatewayConsumer(androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+ field @RequiresPermission(android.Manifest.permission.ACTIVITY_RECOGNITION) public static final int SENSOR_DATA_TYPE_DAILY_STEP_COUNT = 1; // 0x1
+ field @RequiresPermission(android.Manifest.permission.BODY_SENSORS) public static final int SENSOR_DATA_TYPE_HEART_RATE = 0; // 0x0
+ field public static final int SENSOR_DATA_TYPE_INVALID = -1; // 0xffffffff
+ }
+
+ public static interface SensorGateway.Consumer {
+ method public int getRequestedDataType();
+ method @AnyThread public void onData(double);
+ method @AnyThread public default void onInvalidated();
+ method @AnyThread public default void onPreUpdate();
+ }
+
+}
+
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
index e6f50d0..654b5cb 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
@@ -1 +1,58 @@
// Signature format: 4.0
+package androidx.wear.protolayout.expression.pipeline {
+
+ public interface BoundDynamicType extends java.lang.AutoCloseable {
+ method @UiThread public void close();
+ }
+
+ public class DynamicTypeEvaluator implements java.lang.AutoCloseable {
+ ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore);
+ ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager);
+ method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString, android.icu.util.ULocale, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.String!>);
+ method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
+ method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Float!>);
+ method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
+ method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Boolean!>);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void close();
+ method @UiThread public void disablePlatformDataSources();
+ method @UiThread public void enablePlatformDataSources();
+ }
+
+ public interface DynamicTypeValueReceiver<T> {
+ method @UiThread public void onData(T);
+ method @UiThread public void onInvalidated();
+ }
+
+ public class ObservableStateStore {
+ ctor public ObservableStateStore(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue!>);
+ method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue!>);
+ }
+
+ public interface QuotaManager {
+ method public void releaseQuota(int);
+ method public boolean tryAcquireQuota(int);
+ }
+
+}
+
+package androidx.wear.protolayout.expression.pipeline.sensor {
+
+ public interface SensorGateway extends java.io.Closeable {
+ method public void close();
+ method @UiThread public void registerSensorGatewayConsumer(androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+ method @UiThread public void registerSensorGatewayConsumer(java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+ method @UiThread public void unregisterSensorGatewayConsumer(androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+ field @RequiresPermission(android.Manifest.permission.ACTIVITY_RECOGNITION) public static final int SENSOR_DATA_TYPE_DAILY_STEP_COUNT = 1; // 0x1
+ field @RequiresPermission(android.Manifest.permission.BODY_SENSORS) public static final int SENSOR_DATA_TYPE_HEART_RATE = 0; // 0x0
+ field public static final int SENSOR_DATA_TYPE_INVALID = -1; // 0xffffffff
+ }
+
+ public static interface SensorGateway.Consumer {
+ method public int getRequestedDataType();
+ method @AnyThread public void onData(double);
+ method @AnyThread public default void onInvalidated();
+ method @AnyThread public default void onPreUpdate();
+ }
+
+}
+
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
index e6f50d0..25b65f7 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
@@ -1 +1,60 @@
// Signature format: 4.0
+package androidx.wear.protolayout.expression.pipeline {
+
+ public interface BoundDynamicType extends java.lang.AutoCloseable {
+ method @UiThread public void close();
+ }
+
+ public class DynamicTypeEvaluator implements java.lang.AutoCloseable {
+ ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore);
+ ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager);
+ method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString, android.icu.util.ULocale, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.String!>);
+ method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
+ method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Float!>);
+ method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
+ method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Boolean!>);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void close();
+ method @UiThread public void disablePlatformDataSources();
+ method @UiThread public void enablePlatformDataSources();
+ }
+
+ public interface DynamicTypeValueReceiver<T> {
+ method @UiThread public void onData(T);
+ method @UiThread public void onInvalidated();
+ }
+
+ public class ObservableStateStore {
+ ctor public ObservableStateStore(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue!>);
+ method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue!>);
+ }
+
+ public interface QuotaManager {
+ method public void releaseQuota(int);
+ method public boolean tryAcquireQuota(int);
+ }
+
+}
+
+package androidx.wear.protolayout.expression.pipeline.sensor {
+
+ public interface SensorGateway extends java.io.Closeable {
+ method public void close();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void disableUpdates();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void enableUpdates();
+ method @UiThread public void registerSensorGatewayConsumer(androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+ method @UiThread public void registerSensorGatewayConsumer(java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+ method @UiThread public void unregisterSensorGatewayConsumer(androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.Consumer);
+ field @RequiresPermission(android.Manifest.permission.ACTIVITY_RECOGNITION) public static final int SENSOR_DATA_TYPE_DAILY_STEP_COUNT = 1; // 0x1
+ field @RequiresPermission(android.Manifest.permission.BODY_SENSORS) public static final int SENSOR_DATA_TYPE_HEART_RATE = 0; // 0x0
+ field public static final int SENSOR_DATA_TYPE_INVALID = -1; // 0xffffffff
+ }
+
+ public static interface SensorGateway.Consumer {
+ method public int getRequestedDataType();
+ method @AnyThread public void onData(double);
+ method @AnyThread public default void onInvalidated();
+ method @AnyThread public default void onPreUpdate();
+ }
+
+}
+
diff --git a/wear/protolayout/protolayout-expression-pipeline/build.gradle b/wear/protolayout/protolayout-expression-pipeline/build.gradle
index 7a9f216..24961ce 100644
--- a/wear/protolayout/protolayout-expression-pipeline/build.gradle
+++ b/wear/protolayout/protolayout-expression-pipeline/build.gradle
@@ -23,10 +23,28 @@
dependencies {
annotationProcessor(libs.nullaway)
+ api("androidx.annotation:annotation:1.2.0")
+ implementation("androidx.collection:collection:1.2.0")
+
+ implementation("androidx.annotation:annotation-experimental:1.2.0")
+ implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
+ implementation(project(":wear:protolayout:protolayout-expression"))
+
+ compileOnly(libs.kotlinStdlib) // For annotation-experimental
+
+ testImplementation(libs.testExtJunit)
+ testImplementation(libs.testExtTruth)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.truth)
}
android {
namespace "androidx.wear.protolayout.expression.pipeline"
+
+ defaultConfig {
+ minSdkVersion 26
+ }
}
androidx {
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/AnimatableNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/AnimatableNode.java
new file mode 100644
index 0000000..c36521a
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/AnimatableNode.java
@@ -0,0 +1,103 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.UiThread;
+import androidx.annotation.VisibleForTesting;
+
+/** Data animatable source node within a dynamic data pipeline. */
+abstract class AnimatableNode {
+
+ private boolean mIsVisible = false;
+ @NonNull final QuotaAwareAnimator mQuotaAwareAnimator;
+
+ protected AnimatableNode(@NonNull QuotaManager quotaManager) {
+ mQuotaAwareAnimator = new QuotaAwareAnimator(null, quotaManager);
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ AnimatableNode(@NonNull QuotaAwareAnimator quotaAwareAnimator) {
+ mQuotaAwareAnimator = quotaAwareAnimator;
+ }
+
+ /**
+ * Starts the animator (if present) if the node is visible and there is a quota, otherwise, skip
+ * it.
+ */
+ @UiThread
+ @SuppressLint("CheckResult") // (b/247804720)
+ protected void startOrSkipAnimator() {
+ if (mIsVisible) {
+ mQuotaAwareAnimator.tryStartAnimation();
+ } else {
+ stopOrPauseAnimator();
+ }
+ }
+
+ /**
+ * Sets the node's visibility and resumes or stops the corresponding animator (if present).
+ *
+ * <p>If it's becoming visible, paused animations are resumed, other infinite animations that
+ * haven't started yet will start.
+ *
+ * <p>If it's becoming invisible, all animations should skip to end or pause.
+ */
+ @UiThread
+ void setVisibility(boolean visible) {
+ if (mIsVisible == visible) {
+ return;
+ }
+ mIsVisible = visible;
+ if (mIsVisible) {
+ startOrResumeAnimator();
+ } else if (mQuotaAwareAnimator.hasRunningAnimation()) {
+ stopOrPauseAnimator();
+ }
+ }
+
+ /**
+ * Starts or resumes the animator if there is a quota, depending on whether the animation was
+ * paused.
+ */
+ @SuppressLint("CheckResult") // (b/247804720)
+ private void startOrResumeAnimator() {
+ mQuotaAwareAnimator.tryStartOrResumeAnimator();
+ }
+
+ /** Returns whether this node has a running animation. */
+ boolean hasRunningAnimation() {
+ return mQuotaAwareAnimator.hasRunningAnimation();
+ }
+
+ /** Returns whether the animator in this node has an infinite duration. */
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ protected boolean isInfiniteAnimator() {
+ return mQuotaAwareAnimator.isInfiniteAnimator();
+ }
+
+ /**
+ * Pauses the animator in this node if it has infinite duration, stop it otherwise. Note that
+ * this method has no effect on infinite animators that are not running since Animator#pause()
+ * will be a no-op in that case.
+ */
+ private void stopOrPauseAnimator() {
+ mQuotaAwareAnimator.stopOrPauseAnimator();
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/AnimationsHelper.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/AnimationsHelper.java
new file mode 100644
index 0000000..57490a3
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/AnimationsHelper.java
@@ -0,0 +1,165 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import android.animation.ValueAnimator;
+import android.view.animation.Animation;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.PathInterpolator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.CubicBezierEasing;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.RepeatMode;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.Repeatable;
+
+import java.time.Duration;
+import java.util.EnumMap;
+import java.util.Map;
+
+/**
+ * Helper class for Animations in ProtoLayout. It contains helper methods used in rendered and
+ * constants for default values.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class AnimationsHelper {
+
+ private static final Duration DEFAULT_ANIM_DURATION = Duration.ofMillis(300);
+ private static final Interpolator DEFAULT_ANIM_INTERPOLATOR = new LinearInterpolator();
+ private static final Duration DEFAULT_ANIM_DELAY = Duration.ZERO;
+ private static final int DEFAULT_REPEAT_COUNT = 0;
+ private static final int DEFAULT_REPEAT_MODE = ValueAnimator.RESTART;
+ private static final Map<RepeatMode, Integer> sRepeatModeForAnimator =
+ new EnumMap<>(RepeatMode.class);
+
+ static {
+ sRepeatModeForAnimator.put(RepeatMode.REPEAT_MODE_UNKNOWN, DEFAULT_REPEAT_MODE);
+ sRepeatModeForAnimator.put(RepeatMode.REPEAT_MODE_RESTART, ValueAnimator.RESTART);
+ sRepeatModeForAnimator.put(RepeatMode.REPEAT_MODE_REVERSE, ValueAnimator.REVERSE);
+ }
+
+ private AnimationsHelper() {}
+
+ /** Returns the duration from the given {@link AnimationSpec} or default value if not set. */
+ @NonNull
+ public static Duration getDurationOrDefault(@NonNull AnimationSpec spec) {
+ return spec.getDurationMillis() > 0
+ ? Duration.ofMillis(spec.getDurationMillis())
+ : DEFAULT_ANIM_DURATION;
+ }
+
+ /** Returns the delay from the given {@link AnimationSpec} or default value if not set. */
+ @NonNull
+ public static Duration getDelayOrDefault(@NonNull AnimationSpec spec) {
+ return spec.getDelayMillis() > 0
+ ? Duration.ofMillis(spec.getDelayMillis())
+ : DEFAULT_ANIM_DELAY;
+ }
+
+ /**
+ * Returns the easing converted to the Interpolator from the given {@link AnimationSpec} or
+ * default value if not set.
+ */
+ @NonNull
+ public static Interpolator getInterpolatorOrDefault(@NonNull AnimationSpec spec) {
+ Interpolator interpolator = DEFAULT_ANIM_INTERPOLATOR;
+
+ if (spec.hasEasing()) {
+ switch (spec.getEasing().getInnerCase()) {
+ case CUBIC_BEZIER:
+ if (spec.getEasing().hasCubicBezier()) {
+ CubicBezierEasing cbe = spec.getEasing().getCubicBezier();
+ interpolator =
+ new PathInterpolator(
+ cbe.getX1(), cbe.getY1(), cbe.getX2(), cbe.getY2());
+ }
+ break;
+ case INNER_NOT_SET:
+ break;
+ }
+ }
+
+ return interpolator;
+ }
+
+ /**
+ * Returns the repeat count from the given {@link AnimationSpec} or default value if not set.
+ */
+ public static int getRepeatCountOrDefault(@NonNull AnimationSpec spec) {
+ int repeatCount = DEFAULT_REPEAT_COUNT;
+
+ if (spec.hasRepeatable()) {
+ Repeatable repeatable = spec.getRepeatable();
+ if (repeatable.getIterations() <= 0) {
+ repeatCount = ValueAnimator.INFINITE;
+ } else {
+ // -1 because ValueAnimator uses count as number of how many times will animation be
+ // repeated in addition to the first play.
+ repeatCount = repeatable.getIterations() - 1;
+ }
+ }
+
+ return repeatCount;
+ }
+
+ /** Returns the repeat mode from the given {@link AnimationSpec} or default value if not set. */
+ public static int getRepeatModeOrDefault(@NonNull AnimationSpec spec) {
+ int repeatMode = DEFAULT_REPEAT_MODE;
+
+ if (spec.hasRepeatable()) {
+ Repeatable repeatable = spec.getRepeatable();
+ Integer repeatModeFromMap = sRepeatModeForAnimator.get(repeatable.getRepeatMode());
+ if (repeatModeFromMap != null) {
+ repeatMode = repeatModeFromMap;
+ }
+ }
+
+ return repeatMode;
+ }
+
+ /**
+ * Sets animation parameters (duration, delay, easing, repeat mode and count) to the given
+ * animator. These will be values from the given AnimationSpec if they are set and default
+ * values otherwise.
+ */
+ public static void applyAnimationSpecToAnimator(
+ @NonNull ValueAnimator animator, @NonNull AnimationSpec spec) {
+ animator.setDuration(getDurationOrDefault(spec).toMillis());
+ animator.setStartDelay(getDelayOrDefault(spec).toMillis());
+ animator.setInterpolator(getInterpolatorOrDefault(spec));
+ animator.setRepeatCount(getRepeatCountOrDefault(spec));
+ animator.setRepeatMode(getRepeatModeOrDefault(spec));
+ }
+
+ /**
+ * Sets animation parameters (duration, delay, easing, repeat mode and count) to the given
+ * animation. These will be values from the given AnimationSpec if they are set and default
+ * values otherwise.
+ */
+ public static void applyAnimationSpecToAnimation(
+ @NonNull Animation animation, @NonNull AnimationSpec spec) {
+ animation.setDuration(getDurationOrDefault(spec).toMillis());
+ animation.setStartOffset(getDelayOrDefault(spec).toMillis());
+ animation.setInterpolator(getInterpolatorOrDefault(spec));
+ animation.setRepeatCount(getRepeatCountOrDefault(spec));
+ animation.setRepeatMode(getRepeatModeOrDefault(spec));
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoolNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoolNodes.java
new file mode 100644
index 0000000..a5b484f
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoolNodes.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.pipeline;
+
+import android.util.Log;
+
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.expression.proto.DynamicProto;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ComparisonFloatOp;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ComparisonInt32Op;
+import androidx.wear.protolayout.expression.proto.DynamicProto.StateBoolSource;
+import androidx.wear.protolayout.expression.proto.FixedProto.FixedBool;
+
+/** Dynamic data nodes which yield boleans. */
+class BoolNodes {
+ private BoolNodes() {}
+
+ /** Dynamic boolean node that has a fixed value. */
+ static class FixedBoolNode implements DynamicDataSourceNode<Boolean> {
+ private final boolean mValue;
+ private final DynamicTypeValueReceiver<Boolean> mDownstream;
+
+ FixedBoolNode(FixedBool protoNode, DynamicTypeValueReceiver<Boolean> downstream) {
+ mValue = protoNode.getValue();
+ mDownstream = downstream;
+ }
+
+ @Override
+ @UiThread
+ public void preInit() {
+ mDownstream.onPreUpdate();
+ }
+
+ @Override
+ @UiThread
+ public void init() {
+ mDownstream.onData(mValue);
+ }
+
+ @Override
+ @UiThread
+ public void destroy() {}
+ }
+
+ /** Dynamic boolean node that gets value from the state. */
+ static class StateBoolNode extends StateSourceNode<Boolean> {
+ StateBoolNode(
+ ObservableStateStore stateStore,
+ StateBoolSource protoNode,
+ DynamicTypeValueReceiver<Boolean> downstream) {
+ super(
+ stateStore,
+ protoNode.getSourceKey(),
+ se -> se.getBoolVal().getValue(),
+ downstream);
+ }
+ }
+
+ /** Dynamic boolean node that gets value from comparing two integers. */
+ static class ComparisonInt32Node extends DynamicDataBiTransformNode<Integer, Integer, Boolean> {
+ private static final String TAG = "ComparisonInt32Node";
+
+ ComparisonInt32Node(
+ ComparisonInt32Op protoNode, DynamicTypeValueReceiver<Boolean> downstream) {
+ super(
+ downstream,
+ (lhs, rhs) -> {
+ int unboxedLhs = lhs;
+ int unboxedRhs = rhs;
+
+ switch (protoNode.getOperationType()) {
+ case COMPARISON_OP_TYPE_EQUALS:
+ return unboxedLhs == unboxedRhs;
+ case COMPARISON_OP_TYPE_NOT_EQUALS:
+ return unboxedLhs != unboxedRhs;
+ case COMPARISON_OP_TYPE_LESS_THAN:
+ return unboxedLhs < unboxedRhs;
+ case COMPARISON_OP_TYPE_LESS_THAN_OR_EQUAL_TO:
+ return unboxedLhs <= unboxedRhs;
+ case COMPARISON_OP_TYPE_GREATER_THAN:
+ return unboxedLhs > unboxedRhs;
+ case COMPARISON_OP_TYPE_GREATER_THAN_OR_EQUAL_TO:
+ return unboxedLhs >= unboxedRhs;
+ default:
+ Log.e(TAG, "Unknown operation type in ComparisonInt32Node");
+ return false;
+ }
+ });
+ }
+ }
+
+ /** Dynamic boolean node that gets value from comparing two floats. */
+ static class ComparisonFloatNode extends DynamicDataBiTransformNode<Float, Float, Boolean> {
+ private static final String TAG = "ComparisonFloatNode";
+ public static final float EPSILON = 1e-6f;
+
+ ComparisonFloatNode(
+ ComparisonFloatOp protoNode, DynamicTypeValueReceiver<Boolean> downstream) {
+ super(
+ downstream,
+ (lhs, rhs) -> {
+ float unboxedLhs = lhs;
+ float unboxedRhs = rhs;
+
+ switch (protoNode.getOperationType()) {
+ case COMPARISON_OP_TYPE_EQUALS:
+ return equalFloats(unboxedLhs, unboxedRhs);
+ case COMPARISON_OP_TYPE_NOT_EQUALS:
+ return !equalFloats(unboxedLhs, unboxedRhs);
+ case COMPARISON_OP_TYPE_LESS_THAN:
+ return (unboxedLhs < unboxedRhs)
+ && !equalFloats(unboxedLhs, unboxedRhs);
+ case COMPARISON_OP_TYPE_LESS_THAN_OR_EQUAL_TO:
+ return (unboxedLhs < unboxedRhs)
+ || equalFloats(unboxedLhs, unboxedRhs);
+ case COMPARISON_OP_TYPE_GREATER_THAN:
+ return (unboxedLhs > unboxedRhs)
+ && !equalFloats(unboxedLhs, unboxedRhs);
+ case COMPARISON_OP_TYPE_GREATER_THAN_OR_EQUAL_TO:
+ return (unboxedLhs > unboxedRhs)
+ || equalFloats(unboxedLhs, unboxedRhs);
+ default:
+ Log.e(TAG, "Unknown operation type in ComparisonInt32Node");
+ return false;
+ }
+ });
+ }
+
+ private static boolean equalFloats(float lhs, float rhs) {
+ return Math.abs(lhs - rhs) < EPSILON;
+ }
+ }
+
+ /** Dynamic boolean node that gets opposite value from another boolean node. */
+ static class NotBoolOp extends DynamicDataTransformNode<Boolean, Boolean> {
+ NotBoolOp(DynamicTypeValueReceiver<Boolean> downstream) {
+ super(downstream, b -> !b);
+ }
+ }
+
+ /** Dynamic boolean node that gets value from logical operation. */
+ static class LogicalBoolOp extends DynamicDataBiTransformNode<Boolean, Boolean, Boolean> {
+ private static final String TAG = "LogicalBooleanOp";
+
+ LogicalBoolOp(
+ DynamicProto.LogicalBoolOp protoNode,
+ DynamicTypeValueReceiver<Boolean> downstream) {
+ super(
+ downstream,
+ (a, b) -> {
+ switch (protoNode.getOperationType()) {
+ case LOGICAL_OP_TYPE_AND:
+ return a && b;
+ case LOGICAL_OP_TYPE_OR:
+ return a || b;
+ default:
+ Log.e(TAG, "Unknown operation type in LogicalBoolOp");
+ return false;
+ }
+ });
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicType.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicType.java
new file mode 100644
index 0000000..af3b6f8
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicType.java
@@ -0,0 +1,62 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.UiThread;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * An object representing a dynamic type that is being evaluated by {@link
+ * DynamicTypeEvaluator#bind}.
+ */
+public interface BoundDynamicType extends AutoCloseable {
+ /**
+ * Sets the visibility to all animations in this dynamic type. They can be triggered when
+ * visible.
+ *
+ * @hide
+ */
+ @UiThread
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ void setAnimationVisibility(boolean visible);
+
+ /**
+ * Returns the number of currently running animations in this dynamic type.
+ *
+ * @hide
+ */
+ @UiThread
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ int getRunningAnimationCount();
+
+ /** Destroys this dynamic type and it shouldn't be used after this. */
+ @UiThread
+ @Override
+ void close();
+
+ /**
+ * Returns the number of dynamic nodes that this dynamic type contains.
+ *
+ * @hide
+ */
+ @UiThread
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ int getDynamicNodeCount();
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
new file mode 100644
index 0000000..4ab8ecd
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.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.wear.protolayout.expression.pipeline;
+
+import java.util.List;
+
+/**
+ * Dynamic type node implementation that contains a list of {@link DynamicDataNode} created during
+ * evaluation.
+ */
+class BoundDynamicTypeImpl implements BoundDynamicType {
+ private final List<DynamicDataNode<?>> mNodes;
+
+ BoundDynamicTypeImpl(List<DynamicDataNode<?>> nodes) {
+ this.mNodes = nodes;
+ }
+
+ /**
+ * Sets visibility for all {@link AnimatableNode} in this dynamic type. For others, this is
+ * no-op.
+ */
+ @Override
+ public void setAnimationVisibility(boolean visible) {
+ mNodes.stream()
+ .filter(n -> n instanceof AnimatableNode)
+ .forEach(n -> ((AnimatableNode) n).setVisibility(visible));
+ }
+
+ /** Returns how many of {@link AnimatableNode} are running. */
+ @Override
+ public int getRunningAnimationCount() {
+ return (int)
+ mNodes.stream()
+ .filter(n -> n instanceof AnimatableNode)
+ .filter(n -> ((AnimatableNode) n).hasRunningAnimation())
+ .count();
+ }
+
+ @Override
+ public int getDynamicNodeCount() {
+ return mNodes.size();
+ }
+
+ @Override
+ public void close() {
+ mNodes.stream()
+ .filter(n -> n instanceof DynamicDataSourceNode)
+ .forEach(n -> ((DynamicDataSourceNode<?>) n).destroy());
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ColorNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ColorNodes.java
new file mode 100644
index 0000000..54cbc77
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ColorNodes.java
@@ -0,0 +1,196 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import static androidx.wear.protolayout.expression.pipeline.AnimationsHelper.applyAnimationSpecToAnimator;
+
+import android.animation.ValueAnimator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
+import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableFixedColor;
+import androidx.wear.protolayout.expression.proto.DynamicProto.StateColorSource;
+import androidx.wear.protolayout.expression.proto.FixedProto.FixedColor;
+
+/** Dynamic data nodes which yield colors. */
+class ColorNodes {
+ private ColorNodes() {}
+
+ /** Dynamic color node that has a fixed value. */
+ static class FixedColorNode implements DynamicDataSourceNode<Integer> {
+ private final int mValue;
+ private final DynamicTypeValueReceiver<Integer> mDownstream;
+
+ FixedColorNode(FixedColor protoNode, DynamicTypeValueReceiver<Integer> downstream) {
+ this.mValue = protoNode.getArgb();
+ this.mDownstream = downstream;
+ }
+
+ @Override
+ @UiThread
+ public void preInit() {
+ mDownstream.onPreUpdate();
+ }
+
+ @Override
+ @UiThread
+ public void init() {
+ mDownstream.onData(mValue);
+ }
+
+ @Override
+ public void destroy() {}
+ }
+
+ /** Dynamic color node that gets value from the platform source. */
+ static class StateColorSourceNode extends StateSourceNode<Integer> {
+ StateColorSourceNode(
+ ObservableStateStore observableStateStore,
+ StateColorSource protoNode,
+ DynamicTypeValueReceiver<Integer> downstream) {
+ super(
+ observableStateStore,
+ protoNode.getSourceKey(),
+ se -> se.getColorVal().getArgb(),
+ downstream);
+ }
+ }
+
+ /** Dynamic color node that gets animatable value from fixed source. */
+ static class AnimatableFixedColorNode extends AnimatableNode
+ implements DynamicDataSourceNode<Integer> {
+
+ private final AnimatableFixedColor mProtoNode;
+ private final DynamicTypeValueReceiver<Integer> mDownstream;
+
+ AnimatableFixedColorNode(
+ AnimatableFixedColor protoNode,
+ DynamicTypeValueReceiver<Integer> mDownstream,
+ QuotaManager quotaManager) {
+ super(quotaManager);
+ this.mProtoNode = protoNode;
+ this.mDownstream = mDownstream;
+ }
+
+ @Override
+ @UiThread
+ public void preInit() {
+ mDownstream.onPreUpdate();
+ }
+
+ @Override
+ @UiThread
+ public void init() {
+ ValueAnimator animator =
+ ValueAnimator.ofArgb(mProtoNode.getFromArgb(), mProtoNode.getToArgb());
+ animator.addUpdateListener(a -> mDownstream.onData((Integer) a.getAnimatedValue()));
+
+ applyAnimationSpecToAnimator(animator, mProtoNode.getSpec());
+
+ mQuotaAwareAnimator.updateAnimator(animator);
+ startOrSkipAnimator();
+ }
+
+ @Override
+ @UiThread
+ public void destroy() {
+ mQuotaAwareAnimator.stopAnimator();
+ }
+ }
+
+ /** Dynamic color node that gets animatable value from dynamic source. */
+ static class DynamicAnimatedColorNode extends AnimatableNode
+ implements DynamicDataNode<Integer> {
+
+ final DynamicTypeValueReceiver<Integer> mDownstream;
+ private final DynamicTypeValueReceiver<Integer> mInputCallback;
+
+ @Nullable Integer mCurrentValue = null;
+ int mPendingCalls = 0;
+
+ // Static analysis complains about calling methods of parent class AnimatableNode under
+ // initialization but mInputCallback is only used after the constructor is finished.
+ @SuppressWarnings("method.invocation.invalid")
+ DynamicAnimatedColorNode(
+ DynamicTypeValueReceiver<Integer> downstream,
+ @NonNull AnimationSpec spec,
+ QuotaManager quotaManager) {
+ super(quotaManager);
+ this.mDownstream = downstream;
+ this.mInputCallback =
+ new DynamicTypeValueReceiver<Integer>() {
+ @Override
+ public void onPreUpdate() {
+ mPendingCalls++;
+
+ if (mPendingCalls == 1) {
+ mDownstream.onPreUpdate();
+
+ mQuotaAwareAnimator.resetAnimator();
+ }
+ }
+
+ @Override
+ public void onData(@NonNull Integer newData) {
+ if (mPendingCalls > 0) {
+ mPendingCalls--;
+ }
+
+ if (mPendingCalls == 0) {
+ if (mCurrentValue == null) {
+ mCurrentValue = newData;
+ mDownstream.onData(mCurrentValue);
+ } else {
+ ValueAnimator animator =
+ ValueAnimator.ofArgb(mCurrentValue, newData);
+
+ applyAnimationSpecToAnimator(animator, spec);
+ animator.addUpdateListener(
+ a -> {
+ if (mPendingCalls == 0) {
+ mCurrentValue = (Integer) a.getAnimatedValue();
+ mDownstream.onData(mCurrentValue);
+ }
+ });
+
+ mQuotaAwareAnimator.updateAnimator(animator);
+ startOrSkipAnimator();
+ }
+ }
+ }
+
+ @Override
+ public void onInvalidated() {
+ if (mPendingCalls > 0) {
+ mPendingCalls--;
+ }
+
+ if (mPendingCalls == 0) {
+ mCurrentValue = null;
+ mDownstream.onInvalidated();
+ }
+ }
+ };
+ }
+
+ public DynamicTypeValueReceiver<Integer> getInputCallback() {
+ return mInputCallback;
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ConditionalOpNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ConditionalOpNode.java
new file mode 100644
index 0000000..4e53d20
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ConditionalOpNode.java
@@ -0,0 +1,193 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Dynamic data nodes which yield result based on the given condition. */
+class ConditionalOpNode<T> implements DynamicDataNode<T> {
+ private final DynamicTypeValueReceiver<T> mTrueValueIncomingCallback;
+ private final DynamicTypeValueReceiver<T> mFalseValueIncomingCallback;
+ private final DynamicTypeValueReceiver<Boolean> mConditionIncomingCallback;
+
+ final DynamicTypeValueReceiver<T> mDownstream;
+
+ @Nullable Boolean mLastConditional;
+ @Nullable T mLastTrueValue;
+ @Nullable T mLastFalseValue;
+
+ // Counters to track how many "in-flight" updates are pending for each input. If any of these
+ // are >0, then we're still waiting for onData() to be called for one of the callbacks,
+ // so we shouldn't emit any values until they have all resolved.
+ int mPendingConditionalUpdates = 0;
+ int mPendingTrueValueUpdates = 0;
+ int mPendingFalseValueUpdates = 0;
+
+ ConditionalOpNode(DynamicTypeValueReceiver<T> downstream) {
+ mDownstream = downstream;
+
+ // These classes refer to this.handleUpdate, which is @UnderInitialization when these
+ // initializers run, and hence raise an error. It's invalid to annotate
+ // handle{Pre}StateUpdate as @UnderInitialization (since it refers to initialized fields),
+ // and moving this assignment into the constructor yields the same error (since one of the
+ // fields has to be assigned first, when the class is still under initialization).
+ //
+ // The only path to get these is via get*IncomingCallback, which can only be called when the
+ // class is initialized (and which also cannot be called from a sub-constructor, as that
+ // will again complain that it's calling something which is @UnderInitialization). Given
+ // that, suppressing the warning in onData should be safe.
+ mTrueValueIncomingCallback =
+ new DynamicTypeValueReceiver<T>() {
+ @Override
+ public void onPreUpdate() {
+ mPendingTrueValueUpdates++;
+
+ if (mPendingTrueValueUpdates == 1
+ && mPendingFalseValueUpdates == 0
+ && mPendingConditionalUpdates == 0) {
+ mDownstream.onPreUpdate();
+ }
+ }
+
+ @SuppressWarnings("method.invocation")
+ @Override
+ public void onData(@NonNull T newData) {
+ if (mPendingTrueValueUpdates > 0) {
+ mPendingTrueValueUpdates--;
+ }
+
+ mLastTrueValue = newData;
+ handleUpdate();
+ }
+
+ @SuppressWarnings("method.invocation")
+ @Override
+ public void onInvalidated() {
+ if (mPendingTrueValueUpdates > 0) {
+ mPendingTrueValueUpdates--;
+ }
+
+ mLastTrueValue = null;
+ handleUpdate();
+ }
+ };
+
+ mFalseValueIncomingCallback =
+ new DynamicTypeValueReceiver<T>() {
+ @Override
+ public void onPreUpdate() {
+ mPendingFalseValueUpdates++;
+
+ if (mPendingTrueValueUpdates == 0
+ && mPendingFalseValueUpdates == 1
+ && mPendingConditionalUpdates == 0) {
+ mDownstream.onPreUpdate();
+ }
+ }
+
+ @SuppressWarnings("method.invocation")
+ @Override
+ public void onData(@NonNull T newData) {
+ if (mPendingFalseValueUpdates > 0) {
+ mPendingFalseValueUpdates--;
+ }
+
+ mLastFalseValue = newData;
+ handleUpdate();
+ }
+
+ @SuppressWarnings("method.invocation")
+ @Override
+ public void onInvalidated() {
+ if (mPendingFalseValueUpdates > 0) {
+ mPendingFalseValueUpdates--;
+ }
+
+ mLastFalseValue = null;
+ handleUpdate();
+ }
+ };
+
+ mConditionIncomingCallback =
+ new DynamicTypeValueReceiver<Boolean>() {
+ @Override
+ public void onPreUpdate() {
+ mPendingConditionalUpdates++;
+
+ if (mPendingTrueValueUpdates == 0
+ && mPendingFalseValueUpdates == 0
+ && mPendingConditionalUpdates == 1) {
+ mDownstream.onPreUpdate();
+ }
+ }
+
+ @SuppressWarnings("method.invocation")
+ @Override
+ public void onData(@NonNull Boolean newData) {
+ if (mPendingConditionalUpdates > 0) {
+ mPendingConditionalUpdates--;
+ }
+
+ mLastConditional = newData;
+ handleUpdate();
+ }
+
+ @SuppressWarnings("method.invocation")
+ @Override
+ public void onInvalidated() {
+ if (mPendingConditionalUpdates > 0) {
+ mPendingConditionalUpdates--;
+ }
+
+ mLastConditional = null;
+ handleUpdate();
+ }
+ };
+ }
+
+ public DynamicTypeValueReceiver<T> getTrueValueIncomingCallback() {
+ return mTrueValueIncomingCallback;
+ }
+
+ public DynamicTypeValueReceiver<T> getFalseValueIncomingCallback() {
+ return mFalseValueIncomingCallback;
+ }
+
+ public DynamicTypeValueReceiver<Boolean> getConditionIncomingCallback() {
+ return mConditionIncomingCallback;
+ }
+
+ void handleUpdate() {
+ if (mPendingTrueValueUpdates > 0
+ || mPendingFalseValueUpdates > 0
+ || mPendingConditionalUpdates > 0) {
+ return;
+ }
+
+ if (mLastTrueValue == null || mLastFalseValue == null || mLastConditional == null) {
+ mDownstream.onInvalidated();
+ return;
+ }
+
+ if (mLastConditional) {
+ mDownstream.onData(mLastTrueValue);
+ } else {
+ mDownstream.onData(mLastFalseValue);
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataBiTransformNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataBiTransformNode.java
new file mode 100644
index 0000000..8bf5274
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataBiTransformNode.java
@@ -0,0 +1,167 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+/**
+ * Dynamic data node that can perform a transformation from two upstream nodes. This should be
+ * created by passing a {@link Function} in, which implements the transformation.
+ *
+ * <p>The two inputs to this are called the left/right-hand side of the operation, since many of the
+ * operations extending this class are likely to be simple maths operations. Conventionally then,
+ * descendants of this class will implement operations of the form "O = LHS [op] RHS", or "O =
+ * op(LHS, RHS)".
+ *
+ * @param <LhsT> The source data type for the left-hand side of the operation.
+ * @param <RhsT> The source data type for the right-hand side of the operation.
+ * @param <O> The data type that this node emits.
+ */
+class DynamicDataBiTransformNode<LhsT, RhsT, O> implements DynamicDataNode<O> {
+ private static final String TAG = "DynamicDataBiTransform";
+
+ private final DynamicTypeValueReceiver<LhsT> mLhsIncomingCallback;
+ private final DynamicTypeValueReceiver<RhsT> mRhsIncomingCallback;
+
+ final DynamicTypeValueReceiver<O> mDownstream;
+ private final BiFunction<LhsT, RhsT, O> mTransformer;
+
+ @Nullable LhsT mCachedLhsData;
+ @Nullable RhsT mCachedRhsData;
+
+ int mPendingLhsStateUpdates = 0;
+ int mPendingRhsStateUpdates = 0;
+
+ DynamicDataBiTransformNode(
+ DynamicTypeValueReceiver<O> downstream, BiFunction<LhsT, RhsT, O> transformer) {
+ this.mDownstream = downstream;
+ this.mTransformer = transformer;
+
+ // These classes refer to handlePreStateUpdate, which is @UnderInitialization when these
+ // initializers run, and hence raise an error. It's invalid to annotate
+ // handle{Pre}StateUpdate as @UnderInitialization (since it refers to initialized fields),
+ // and moving this assignment into the constructor yields the same error (since one of the
+ // fields has to be assigned first, when the class is still under initialization).
+ //
+ // The only path to get these is via get{Lhs,Rhs}IncomingCallback, which can only be called
+ // when the class is initialized (and which also cannot be called from a sub-constructor, as
+ // that will again complain that it's calling something which is @UnderInitialization).
+ // Given that, suppressing the warning in onStateUpdate should be safe.
+ this.mLhsIncomingCallback =
+ new DynamicTypeValueReceiver<LhsT>() {
+ @Override
+ public void onPreUpdate() {
+ mPendingLhsStateUpdates++;
+
+ if (mPendingLhsStateUpdates == 1 && mPendingRhsStateUpdates == 0) {
+ mDownstream.onPreUpdate();
+ }
+ }
+
+ @SuppressWarnings("method.invocation")
+ @Override
+ public void onData(@NonNull LhsT newData) {
+ onUpdatedImpl(newData);
+ }
+
+ private void onUpdatedImpl(@Nullable LhsT newData) {
+ if (mPendingLhsStateUpdates == 0) {
+ Log.w(
+ TAG,
+ "Received a state update, but one or more suppliers did not"
+ + " call onPreStateUpdate");
+ } else {
+ mPendingLhsStateUpdates--;
+ }
+
+ mCachedLhsData = newData;
+ handleStateUpdate();
+ }
+
+ @Override
+ public void onInvalidated() {
+ // Note: Casts are required here to help out the null checker.
+ onUpdatedImpl((LhsT) null);
+ }
+ };
+
+ this.mRhsIncomingCallback =
+ new DynamicTypeValueReceiver<RhsT>() {
+ @Override
+ public void onPreUpdate() {
+ mPendingRhsStateUpdates++;
+
+ if (mPendingLhsStateUpdates == 0 && mPendingRhsStateUpdates == 1) {
+ mDownstream.onPreUpdate();
+ }
+ }
+
+ @SuppressWarnings("method.invocation")
+ @Override
+ public void onData(@NonNull RhsT newData) {
+ onUpdatedImpl(newData);
+ }
+
+ private void onUpdatedImpl(@Nullable RhsT newData) {
+ if (mPendingRhsStateUpdates == 0) {
+ Log.w(
+ TAG,
+ "Received a state update, but one or more suppliers did not"
+ + " call onPreStateUpdate");
+ } else {
+ mPendingRhsStateUpdates--;
+ }
+
+ mCachedRhsData = newData;
+ handleStateUpdate();
+ }
+
+ @Override
+ public void onInvalidated() {
+ onUpdatedImpl((RhsT) null);
+ }
+ };
+ }
+
+ void handleStateUpdate() {
+ if (mPendingLhsStateUpdates == 0 && mPendingRhsStateUpdates == 0) {
+ LhsT lhs = mCachedLhsData;
+ RhsT rhs = mCachedRhsData;
+
+ if (lhs == null || rhs == null) {
+ mDownstream.onInvalidated();
+ } else {
+ O result = mTransformer.apply(lhs, rhs);
+ mDownstream.onData(result);
+ }
+ }
+ }
+
+ public DynamicTypeValueReceiver<LhsT> getLhsIncomingCallback() {
+ return mLhsIncomingCallback;
+ }
+
+ public DynamicTypeValueReceiver<RhsT> getRhsIncomingCallback() {
+ return mRhsIncomingCallback;
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataNode.java
new file mode 100644
index 0000000..a454577
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataNode.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.pipeline;
+
+/**
+ * Node within a dynamic data pipeline.
+ *
+ * <p>Each node should either be a {@link DynamicDataSourceNode}, in which case it pushes data into
+ * the pipeline, or it should expose one or more callbacks (generally instances of {@link
+ * DynamicTypeValueReceiver}), which can be used by an upstream node to "push" data through the
+ * pipeline. A node would typically look like the following:
+ *
+ * <pre>{@code
+ * class IntToStringNode implements DynamicDataNode<String> {
+ * // The consumer on the downstream node to push data to.
+ * private final DynamicTypeValueReceiver<String> downstreamNode;
+ *
+ * private final DynamicTypeValueReceiver<Integer> myNode =
+ * new DynamicTypeValueReceiver<Integer>() {
+ * @Override
+ * public void onPreStateUpdate() {
+ * // Don't need to do anything here; just relay.
+ * downstreamNode.onPreStateUpdate();
+ * }
+ *
+ * @Override
+ * public void onStateUpdate(Integer newData) {
+ * downstreamNode.onStateUpdate(newData.toString());
+ * }
+ * };
+ *
+ * public DynamicTypeValueReceiver<Integer> getConsumer() { return myNode; }
+ * }
+ * }</pre>
+ *
+ * An upstream node (i.e. one which yields an Integer) would then push data in to this node, via the
+ * consumer returned from {@code IntToStringNode#getConsumer}.
+ *
+ * <p>Generally, node implementations will not use this interface directly; {@link
+ * DynamicDataTransformNode} and {@link DynamicDataBiTransformNode} provide canonical
+ * implementations for transforming data pushed from one or two source nodes, to a downstream node.
+ *
+ * @param <O> The data type that this node yields.
+ */
+interface DynamicDataNode<O> {}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataSourceNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataSourceNode.java
new file mode 100644
index 0000000..7e770dc
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataSourceNode.java
@@ -0,0 +1,47 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.UiThread;
+
+/**
+ * Data source node within a dynamic data pipeline. This node type should push data into the
+ * pipeline, either once when the pipeline is inited, or periodically from its data source (e.g. for
+ * sensor nodes).
+ *
+ * @param <T> The type of data this node emits.
+ */
+interface DynamicDataSourceNode<T> extends DynamicDataNode<T> {
+ /**
+ * Called on all source nodes before {@link DynamicDataSourceNode#init()} is called on any node.
+ * This should generally only call {@link DynamicTypeValueReceiver#onPreUpdate()} on all
+ * downstream nodes.
+ */
+ @UiThread
+ void preInit();
+
+ /**
+ * Initialize this node. This should cause it to bind to any data sources, and emit its first
+ * value.
+ */
+ @UiThread
+ void init();
+
+ /** Destroy this node. This should cause it to unbind from any data sources. */
+ @UiThread
+ void destroy();
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java
new file mode 100644
index 0000000..f9e476a
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.NonNull;
+
+import java.util.function.Function;
+
+/**
+ * Dynamic data node that can perform a transformation from an upstream node. This should be created
+ * by passing a {@link Function} in, which implements the transformation
+ *
+ * @param <I> The source data type of this node.
+ * @param <O> The data type that this node emits.
+ */
+class DynamicDataTransformNode<I, O> implements DynamicDataNode<O> {
+ private final DynamicTypeValueReceiver<I> mCallback;
+
+ final DynamicTypeValueReceiver<O> mDownstream;
+ final Function<I, O> mTransformer;
+
+ DynamicDataTransformNode(DynamicTypeValueReceiver<O> downstream, Function<I, O> transformer) {
+ this.mDownstream = downstream;
+ this.mTransformer = transformer;
+
+ mCallback =
+ new DynamicTypeValueReceiver<I>() {
+ @Override
+ public void onPreUpdate() {
+ // Don't need to do anything here; just relay.
+ mDownstream.onPreUpdate();
+ }
+
+ @Override
+ public void onData(@NonNull I newData) {
+ O result = mTransformer.apply(newData);
+ mDownstream.onData(result);
+ }
+
+ @Override
+ public void onInvalidated() {
+ mDownstream.onInvalidated();
+ }
+ };
+ }
+
+ public DynamicTypeValueReceiver<I> getIncomingCallback() {
+ return mCallback;
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
new file mode 100644
index 0000000..35936c3
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
@@ -0,0 +1,985 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import android.icu.util.ULocale;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.expression.DynamicBuilders;
+import androidx.wear.protolayout.expression.pipeline.BoolNodes.ComparisonFloatNode;
+import androidx.wear.protolayout.expression.pipeline.BoolNodes.ComparisonInt32Node;
+import androidx.wear.protolayout.expression.pipeline.BoolNodes.FixedBoolNode;
+import androidx.wear.protolayout.expression.pipeline.BoolNodes.LogicalBoolOp;
+import androidx.wear.protolayout.expression.pipeline.BoolNodes.NotBoolOp;
+import androidx.wear.protolayout.expression.pipeline.BoolNodes.StateBoolNode;
+import androidx.wear.protolayout.expression.pipeline.ColorNodes.AnimatableFixedColorNode;
+import androidx.wear.protolayout.expression.pipeline.ColorNodes.DynamicAnimatedColorNode;
+import androidx.wear.protolayout.expression.pipeline.ColorNodes.FixedColorNode;
+import androidx.wear.protolayout.expression.pipeline.ColorNodes.StateColorSourceNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.AnimatableFixedFloatNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.ArithmeticFloatNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.DynamicAnimatedFloatNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.FixedFloatNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.Int32ToFloatNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.StateFloatNode;
+import androidx.wear.protolayout.expression.pipeline.Int32Nodes.ArithmeticInt32Node;
+import androidx.wear.protolayout.expression.pipeline.Int32Nodes.FixedInt32Node;
+import androidx.wear.protolayout.expression.pipeline.Int32Nodes.FloatToInt32Node;
+import androidx.wear.protolayout.expression.pipeline.Int32Nodes.PlatformInt32SourceNode;
+import androidx.wear.protolayout.expression.pipeline.Int32Nodes.StateInt32SourceNode;
+import androidx.wear.protolayout.expression.pipeline.PlatformDataSources.EpochTimePlatformDataSource;
+import androidx.wear.protolayout.expression.pipeline.PlatformDataSources.SensorGatewayPlatformDataSource;
+import androidx.wear.protolayout.expression.pipeline.StringNodes.FixedStringNode;
+import androidx.wear.protolayout.expression.pipeline.StringNodes.FloatFormatNode;
+import androidx.wear.protolayout.expression.pipeline.StringNodes.Int32FormatNode;
+import androidx.wear.protolayout.expression.pipeline.StringNodes.StateStringNode;
+import androidx.wear.protolayout.expression.pipeline.StringNodes.StringConcatOpNode;
+import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway;
+import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableDynamicColor;
+import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableDynamicFloat;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ConditionalFloatOp;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ConditionalInt32Op;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ConditionalStringOp;
+import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicBool;
+import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicColor;
+import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicFloat;
+import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicInt32;
+import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicString;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Evaluates protolayout dynamic types.
+ *
+ * <p>Given a dynamic ProtoLayout data source, this builds up a sequence of {@link DynamicDataNode}
+ * instances, which can source the required data, and transform it into its final form.
+ *
+ * <p>Data source can includes animations which will then emit value transitions.
+ *
+ * <p>In order to evaluate dynamic types, the caller needs to add any number of pending dynamic
+ * types with {@link #bind} methods and then call {@link #processPendingBindings()} to start
+ * evaluation on those dynamic types. Starting evaluation can be done for batches of dynamic types.
+ *
+ * <p>It's the callers responsibility to destroy those dynamic types after use, with {@link
+ * BoundDynamicType#close()}.
+ */
+public class DynamicTypeEvaluator implements AutoCloseable {
+ private static final String TAG = "DynamicTypeEvaluator";
+
+ @Nullable private final SensorGateway mSensorGateway;
+ @Nullable private final SensorGatewayPlatformDataSource mSensorGatewayDataSource;
+ @NonNull private final TimeGatewayImpl mTimeGateway;
+ @Nullable private final EpochTimePlatformDataSource mTimeDataSource;
+ @NonNull private final ObservableStateStore mStateStore;
+ private final boolean mEnableAnimations;
+ @NonNull private final QuotaManager mAnimationQuotaManager;
+ @NonNull private final List<DynamicDataNode<?>> mDynamicTypeNodes = new ArrayList<>();
+
+ @NonNull
+ private static final QuotaManager DISABLED_ANIMATIONS_QUOTA_MANAGER =
+ new QuotaManager() {
+ @Override
+ public boolean tryAcquireQuota(int quotaNum) {
+ return false;
+ }
+
+ @Override
+ public void releaseQuota(int quotaNum) {
+ throw new IllegalStateException(
+ "releaseQuota method is called when no quota is acquired!");
+ }
+ };
+
+ /**
+ * Creates a {@link DynamicTypeEvaluator} without animation support.
+ *
+ * @param platformDataSourcesInitiallyEnabled Whether sending updates from sensor and time
+ * sources should be allowed initially. After that, enabling updates from sensor and time
+ * sources can be done via {@link #enablePlatformDataSources()} or {@link
+ * #disablePlatformDataSources()}.
+ * @param sensorGateway The gateway for sensor data.
+ * @param stateStore The state store that will be used for dereferencing the state keys in the
+ * dynamic types.
+ */
+ public DynamicTypeEvaluator(
+ boolean platformDataSourcesInitiallyEnabled,
+ @Nullable SensorGateway sensorGateway,
+ @NonNull ObservableStateStore stateStore) {
+ // Build pipeline with quota that doesn't allow any animations.
+ this(
+ platformDataSourcesInitiallyEnabled,
+ sensorGateway,
+ stateStore,
+ /* enableAnimations= */ false,
+ DISABLED_ANIMATIONS_QUOTA_MANAGER);
+ }
+
+ /**
+ * Creates a {@link DynamicTypeEvaluator} with animation support. Maximum number of concurrently
+ * running animations is defined in the given {@link QuotaManager}. Passing in animatable data
+ * source to any of the methods will emit value transitions, for example animatable float from 5
+ * to 10 will emit all values between those numbers (i.e. 5, 6, 7, 8, 9, 10).
+ *
+ * @param platformDataSourcesInitiallyEnabled Whether sending updates from sensor and time
+ * sources should be allowed initially. After that, enabling updates from sensor and time
+ * sources can be done via {@link #enablePlatformDataSources()} or {@link
+ * #disablePlatformDataSources()}.
+ * @param sensorGateway The gateway for sensor data.
+ * @param stateStore The state store that will be used for dereferencing the state keys in the
+ * dynamic types.
+ * @param animationQuotaManager The quota manager used for limiting the number of concurrently
+ * running animations.
+ */
+ public DynamicTypeEvaluator(
+ boolean platformDataSourcesInitiallyEnabled,
+ @Nullable SensorGateway sensorGateway,
+ @NonNull ObservableStateStore stateStore,
+ @NonNull QuotaManager animationQuotaManager) {
+ this(
+ platformDataSourcesInitiallyEnabled,
+ sensorGateway,
+ stateStore,
+ /* enableAnimations= */ true,
+ animationQuotaManager);
+ }
+
+ /**
+ * Creates a {@link DynamicTypeEvaluator}.
+ *
+ * @param platformDataSourcesInitiallyEnabled Whether sending updates from sensor and time
+ * sources should be allowed initially. After that, enabling updates from sensor and time
+ * sources can be done via {@link #enablePlatformDataSources()} or {@link
+ * #disablePlatformDataSources()}.
+ * @param sensorGateway The gateway for sensor data.
+ * @param stateStore The state store that will be used for dereferencing the state keys in the
+ * dynamic types.
+ * @param animationQuotaManager The quota manager used for limiting the number of concurrently
+ * running animations.
+ */
+ private DynamicTypeEvaluator(
+ boolean platformDataSourcesInitiallyEnabled,
+ @Nullable SensorGateway sensorGateway,
+ @NonNull ObservableStateStore stateStore,
+ boolean enableAnimations,
+ @NonNull QuotaManager animationQuotaManager) {
+
+ this.mSensorGateway = sensorGateway;
+ Handler uiHandler = new Handler(Looper.getMainLooper());
+ Executor uiExecutor = new MainThreadExecutor(uiHandler);
+ if (this.mSensorGateway != null) {
+ if (platformDataSourcesInitiallyEnabled) {
+ this.mSensorGateway.enableUpdates();
+ } else {
+ this.mSensorGateway.disableUpdates();
+ }
+ this.mSensorGatewayDataSource =
+ new SensorGatewayPlatformDataSource(uiExecutor, this.mSensorGateway);
+ } else {
+ this.mSensorGatewayDataSource = null;
+ }
+
+ this.mTimeGateway = new TimeGatewayImpl(uiHandler, platformDataSourcesInitiallyEnabled);
+ this.mTimeDataSource = new EpochTimePlatformDataSource(uiExecutor, mTimeGateway);
+
+ this.mEnableAnimations = enableAnimations;
+ this.mStateStore = stateStore;
+ this.mAnimationQuotaManager = animationQuotaManager;
+ }
+
+ /**
+ * Starts evaluating all stored pending dynamic types.
+ *
+ * <p>This needs to be called when new pending dynamic types are added via any {@code bind}
+ * method, either when one or a batch is added.
+ *
+ * <p>Any pending dynamic type will be initialized for evaluation. All other already initialized
+ * dynamic types will remain unaffected.
+ *
+ * <p>It's the callers responsibility to destroy those dynamic types after use, with {@link
+ * BoundDynamicType#close()}.
+ *
+ * @hide
+ */
+ @UiThread
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public void processPendingBindings() {
+ processBindings(mDynamicTypeNodes);
+
+ // This method empties the array with dynamic type nodes.
+ clearDynamicTypesArray();
+ }
+
+ @UiThread
+ private static void processBindings(List<DynamicDataNode<?>> bindings) {
+ preInitNodes(bindings);
+ initNodes(bindings);
+ }
+
+ /**
+ * Removes any stored pending bindings by clearing the list that stores them. Note that this
+ * doesn't destroy them.
+ */
+ @UiThread
+ private void clearDynamicTypesArray() {
+ mDynamicTypeNodes.clear();
+ }
+
+ /** This should be called before initNodes() */
+ @UiThread
+ private static void preInitNodes(List<DynamicDataNode<?>> bindings) {
+ bindings.stream()
+ .filter(n -> n instanceof DynamicDataSourceNode)
+ .forEach(n -> ((DynamicDataSourceNode<?>) n).preInit());
+ }
+
+ @UiThread
+ private static void initNodes(List<DynamicDataNode<?>> bindings) {
+ bindings.stream()
+ .filter(n -> n instanceof DynamicDataSourceNode)
+ .forEach(n -> ((DynamicDataSourceNode<?>) n).init());
+ }
+
+ /**
+ * Adds dynamic type from the given {@link DynamicBuilders.DynamicString} for evaluation.
+ * Evaluation will start immediately.
+ *
+ * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+ *
+ * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+ * by caller, results of evaluation will be sent through the given {@link
+ * DynamicTypeValueReceiver}.
+ *
+ * @param stringSource The given String dynamic type that should be evaluated.
+ * @param consumer The registered consumer for results of the evaluation. It will be called from
+ * UI thread.
+ * @param locale The locale used for the given String source.
+ */
+ @NonNull
+ public BoundDynamicType bind(
+ @NonNull DynamicBuilders.DynamicString stringSource,
+ @NonNull ULocale locale,
+ @NonNull DynamicTypeValueReceiver<String> consumer) {
+ List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+ bindRecursively(stringSource.toDynamicStringProto(), consumer, locale, resultBuilder);
+ processBindings(resultBuilder);
+ return new BoundDynamicTypeImpl(resultBuilder);
+ }
+
+ /**
+ * Adds pending dynamic type from the given {@link DynamicString} for future evaluation.
+ *
+ * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+ *
+ * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+ * by caller, results of evaluation will be sent through the given {@link
+ * DynamicTypeValueReceiver}.
+ *
+ * @param stringSource The given String dynamic type that should be evaluated.
+ * @param consumer The registered consumer for results of the evaluation. It will be called from
+ * UI thread.
+ * @param locale The locale used for the given String source.
+ * @hide
+ */
+ @NonNull
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public BoundDynamicType bind(
+ @NonNull DynamicString stringSource,
+ @NonNull ULocale locale,
+ @NonNull DynamicTypeValueReceiver<String> consumer) {
+ List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+ bindRecursively(stringSource, consumer, locale, resultBuilder);
+ mDynamicTypeNodes.addAll(resultBuilder);
+ return new BoundDynamicTypeImpl(resultBuilder);
+ }
+
+ /**
+ * Adds dynamic type from the given {@link DynamicBuilders.DynamicInt32} for evaluation.
+ * Evaluation will start immediately.
+ *
+ * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+ *
+ * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+ * by caller, results of evaluation will be sent through the given {@link
+ * DynamicTypeValueReceiver}.
+ *
+ * @param int32Source The given integer dynamic type that should be evaluated.
+ * @param consumer The registered consumer for results of the evaluation. It will be called from
+ * UI thread.
+ */
+ @NonNull
+ public BoundDynamicType bind(
+ @NonNull DynamicBuilders.DynamicInt32 int32Source,
+ @NonNull DynamicTypeValueReceiver<Integer> consumer) {
+ List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+ bindRecursively(int32Source.toDynamicInt32Proto(), consumer, resultBuilder);
+ processBindings(resultBuilder);
+ return new BoundDynamicTypeImpl(resultBuilder);
+ }
+
+ /**
+ * Adds pending dynamic type from the given {@link DynamicInt32} for future evaluation.
+ *
+ * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+ *
+ * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+ * by caller, results of evaluation will be sent through the given {@link
+ * DynamicTypeValueReceiver}.
+ *
+ * @param int32Source The given integer dynamic type that should be evaluated.
+ * @param consumer The registered consumer for results of the evaluation. It will be called from
+ * UI thread.
+ * @hide
+ */
+ @NonNull
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public BoundDynamicType bind(
+ @NonNull DynamicInt32 int32Source,
+ @NonNull DynamicTypeValueReceiver<Integer> consumer) {
+ List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+ bindRecursively(int32Source, consumer, resultBuilder);
+ mDynamicTypeNodes.addAll(resultBuilder);
+ return new BoundDynamicTypeImpl(resultBuilder);
+ }
+
+ /**
+ * Adds dynamic type from the given {@link DynamicBuilders.DynamicFloat} for evaluation.
+ * Evaluation will start immediately.
+ *
+ * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+ *
+ * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+ * by caller, results of evaluation will be sent through the given {@link
+ * DynamicTypeValueReceiver}.
+ *
+ * @param floatSource The given float dynamic type that should be evaluated.
+ * @param consumer The registered consumer for results of the evaluation. It will be called from
+ * UI thread.
+ */
+ @NonNull
+ public BoundDynamicType bind(
+ @NonNull DynamicBuilders.DynamicFloat floatSource,
+ @NonNull DynamicTypeValueReceiver<Float> consumer) {
+ List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+ bindRecursively(floatSource.toDynamicFloatProto(), consumer, resultBuilder);
+ processBindings(resultBuilder);
+ return new BoundDynamicTypeImpl(resultBuilder);
+ }
+
+ /**
+ * Adds pending dynamic type from the given {@link DynamicFloat} for future evaluation.
+ *
+ * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+ *
+ * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+ * by caller, results of evaluation will be sent through the given {@link
+ * DynamicTypeValueReceiver}.
+ *
+ * @param floatSource The given float dynamic type that should be evaluated.
+ * @param consumer The registered consumer for results of the evaluation. It will be called from
+ * UI thread.
+ * @hide
+ */
+ @NonNull
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public BoundDynamicType bind(
+ @NonNull DynamicFloat floatSource, @NonNull DynamicTypeValueReceiver<Float> consumer) {
+ List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+ bindRecursively(floatSource, consumer, resultBuilder);
+ mDynamicTypeNodes.addAll(resultBuilder);
+ return new BoundDynamicTypeImpl(resultBuilder);
+ }
+
+ /**
+ * Adds dynamic type from the given {@link DynamicBuilders.DynamicColor} for evaluation.
+ * Evaluation will start immediately.
+ *
+ * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+ *
+ * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+ * by caller, results of evaluation will be sent through the given {@link
+ * DynamicTypeValueReceiver}.
+ *
+ * @param colorSource The given color dynamic type that should be evaluated.
+ * @param consumer The registered consumer for results of the evaluation. It will be called from
+ * UI thread.
+ */
+ @NonNull
+ public BoundDynamicType bind(
+ @NonNull DynamicBuilders.DynamicColor colorSource,
+ @NonNull DynamicTypeValueReceiver<Integer> consumer) {
+ List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+ bindRecursively(colorSource.toDynamicColorProto(), consumer, resultBuilder);
+ processBindings(resultBuilder);
+ return new BoundDynamicTypeImpl(resultBuilder);
+ }
+
+ /**
+ * Adds pending dynamic type from the given {@link DynamicColor} for future evaluation.
+ *
+ * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+ *
+ * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+ * by caller, results of evaluation will be sent through the given {@link
+ * DynamicTypeValueReceiver}.
+ *
+ * @param colorSource The given color dynamic type that should be evaluated.
+ * @param consumer The registered consumer for results of the evaluation. It will be called from
+ * UI thread.
+ * @hide
+ */
+ @NonNull
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public BoundDynamicType bind(
+ @NonNull DynamicColor colorSource,
+ @NonNull DynamicTypeValueReceiver<Integer> consumer) {
+ List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+ bindRecursively(colorSource, consumer, resultBuilder);
+ mDynamicTypeNodes.addAll(resultBuilder);
+ return new BoundDynamicTypeImpl(resultBuilder);
+ }
+
+ /**
+ * Adds dynamic type from the given {@link DynamicBuilders.DynamicBool} for evaluation.
+ * Evaluation will start immediately.
+ *
+ * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+ *
+ * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+ * by caller, results of evaluation will be sent through the given {@link
+ * DynamicTypeValueReceiver}.
+ *
+ * @param boolSource The given boolean dynamic type that should be evaluated.
+ * @param consumer The registered consumer for results of the evaluation. It will be called from
+ * UI thread.
+ */
+ @NonNull
+ public BoundDynamicType bind(
+ @NonNull DynamicBuilders.DynamicBool boolSource,
+ @NonNull DynamicTypeValueReceiver<Boolean> consumer) {
+ List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+ bindRecursively(boolSource.toDynamicBoolProto(), consumer, resultBuilder);
+ processBindings(resultBuilder);
+ return new BoundDynamicTypeImpl(resultBuilder);
+ }
+
+ /**
+ * Adds pending dynamic type from the given {@link DynamicBool} for future evaluation.
+ *
+ * <p>Evaluation of this dynamic type will start when {@link #processPendingBindings} is called.
+ *
+ * <p>While the {@link BoundDynamicType} is not destroyed with {@link BoundDynamicType#close()}
+ * by caller, results of evaluation will be sent through the given {@link
+ * DynamicTypeValueReceiver}.
+ *
+ * @param boolSource The given boolean dynamic type that should be evaluated.
+ * @param consumer The registered consumer for results of the evaluation. It will be called from
+ * UI thread.
+ * @hide
+ */
+ @NonNull
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public BoundDynamicType bind(
+ @NonNull DynamicBool boolSource, @NonNull DynamicTypeValueReceiver<Boolean> consumer) {
+ List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+ bindRecursively(boolSource, consumer, resultBuilder);
+ mDynamicTypeNodes.addAll(resultBuilder);
+ return new BoundDynamicTypeImpl(resultBuilder);
+ }
+
+ /**
+ * Same as {@link #bind(DynamicBuilders.DynamicString, ULocale, DynamicTypeValueReceiver)}, but
+ * instead of returning one {@link BoundDynamicType}, all {@link DynamicDataNode} produced by
+ * evaluating given dynamic type are added to the given list.
+ */
+ private void bindRecursively(
+ @NonNull DynamicString stringSource,
+ @NonNull DynamicTypeValueReceiver<String> consumer,
+ @NonNull ULocale locale,
+ @NonNull List<DynamicDataNode<?>> resultBuilder) {
+ DynamicDataNode<?> node;
+
+ switch (stringSource.getInnerCase()) {
+ case FIXED:
+ node = new FixedStringNode(stringSource.getFixed(), consumer);
+ break;
+ case INT32_FORMAT_OP:
+ {
+ NumberFormatter formatter =
+ new NumberFormatter(stringSource.getInt32FormatOp(), locale);
+ Int32FormatNode int32FormatNode = new Int32FormatNode(formatter, consumer);
+ node = int32FormatNode;
+ bindRecursively(
+ stringSource.getInt32FormatOp().getInput(),
+ int32FormatNode.getIncomingCallback(),
+ resultBuilder);
+ break;
+ }
+ case FLOAT_FORMAT_OP:
+ {
+ NumberFormatter formatter =
+ new NumberFormatter(stringSource.getFloatFormatOp(), locale);
+ FloatFormatNode floatFormatNode = new FloatFormatNode(formatter, consumer);
+ node = floatFormatNode;
+ bindRecursively(
+ stringSource.getFloatFormatOp().getInput(),
+ floatFormatNode.getIncomingCallback(),
+ resultBuilder);
+ break;
+ }
+ case STATE_SOURCE:
+ {
+ node =
+ new StateStringNode(
+ mStateStore, stringSource.getStateSource(), consumer);
+ break;
+ }
+ case CONDITIONAL_OP:
+ {
+ ConditionalOpNode<String> conditionalNode = new ConditionalOpNode<>(consumer);
+
+ ConditionalStringOp op = stringSource.getConditionalOp();
+ bindRecursively(
+ op.getCondition(),
+ conditionalNode.getConditionIncomingCallback(),
+ resultBuilder);
+ bindRecursively(
+ op.getValueIfTrue(),
+ conditionalNode.getTrueValueIncomingCallback(),
+ locale,
+ resultBuilder);
+ bindRecursively(
+ op.getValueIfFalse(),
+ conditionalNode.getFalseValueIncomingCallback(),
+ locale,
+ resultBuilder);
+
+ node = conditionalNode;
+ break;
+ }
+ case CONCAT_OP:
+ {
+ StringConcatOpNode concatNode = new StringConcatOpNode(consumer);
+ node = concatNode;
+ bindRecursively(
+ stringSource.getConcatOp().getInputLhs(),
+ concatNode.getLhsIncomingCallback(),
+ locale,
+ resultBuilder);
+ bindRecursively(
+ stringSource.getConcatOp().getInputRhs(),
+ concatNode.getRhsIncomingCallback(),
+ locale,
+ resultBuilder);
+ break;
+ }
+ case INNER_NOT_SET:
+ throw new IllegalArgumentException("DynamicString has no inner source set");
+ default:
+ throw new IllegalArgumentException("Unknown DynamicString source type");
+ }
+
+ resultBuilder.add(node);
+ }
+
+ /**
+ * Same as {@link #bind(DynamicBuilders.DynamicInt32, DynamicTypeValueReceiver)}, all {@link
+ * DynamicDataNode} produced by evaluating given dynamic type are added to the given list.
+ */
+ private void bindRecursively(
+ @NonNull DynamicInt32 int32Source,
+ @NonNull DynamicTypeValueReceiver<Integer> consumer,
+ @NonNull List<DynamicDataNode<?>> resultBuilder) {
+ DynamicDataNode<Integer> node;
+
+ switch (int32Source.getInnerCase()) {
+ case FIXED:
+ node = new FixedInt32Node(int32Source.getFixed(), consumer);
+ break;
+ case PLATFORM_SOURCE:
+ node =
+ new PlatformInt32SourceNode(
+ int32Source.getPlatformSource(),
+ mTimeDataSource,
+ mSensorGatewayDataSource,
+ consumer);
+ break;
+ case ARITHMETIC_OPERATION:
+ {
+ ArithmeticInt32Node arithmeticNode =
+ new ArithmeticInt32Node(int32Source.getArithmeticOperation(), consumer);
+ node = arithmeticNode;
+
+ bindRecursively(
+ int32Source.getArithmeticOperation().getInputLhs(),
+ arithmeticNode.getLhsIncomingCallback(),
+ resultBuilder);
+ bindRecursively(
+ int32Source.getArithmeticOperation().getInputRhs(),
+ arithmeticNode.getRhsIncomingCallback(),
+ resultBuilder);
+
+ break;
+ }
+ case STATE_SOURCE:
+ {
+ node =
+ new StateInt32SourceNode(
+ mStateStore, int32Source.getStateSource(), consumer);
+ break;
+ }
+ case CONDITIONAL_OP:
+ {
+ ConditionalOpNode<Integer> conditionalNode = new ConditionalOpNode<>(consumer);
+
+ ConditionalInt32Op op = int32Source.getConditionalOp();
+ bindRecursively(
+ op.getCondition(),
+ conditionalNode.getConditionIncomingCallback(),
+ resultBuilder);
+ bindRecursively(
+ op.getValueIfTrue(),
+ conditionalNode.getTrueValueIncomingCallback(),
+ resultBuilder);
+ bindRecursively(
+ op.getValueIfFalse(),
+ conditionalNode.getFalseValueIncomingCallback(),
+ resultBuilder);
+
+ node = conditionalNode;
+ break;
+ }
+ case FLOAT_TO_INT:
+ {
+ FloatToInt32Node conversionNode =
+ new FloatToInt32Node(int32Source.getFloatToInt(), consumer);
+ node = conversionNode;
+
+ bindRecursively(
+ int32Source.getFloatToInt().getInput(),
+ conversionNode.getIncomingCallback(),
+ resultBuilder);
+ break;
+ }
+ case INNER_NOT_SET:
+ throw new IllegalArgumentException("DynamicInt32 has no inner source set");
+ default:
+ throw new IllegalArgumentException("Unknown DynamicInt32 source type");
+ }
+
+ resultBuilder.add(node);
+ }
+
+ /**
+ * Same as {@link #bind(DynamicBuilders.DynamicFloat, DynamicTypeValueReceiver)}, all {@link
+ * DynamicDataNode} produced by evaluating given dynamic type are added to the given list.
+ */
+ private void bindRecursively(
+ @NonNull DynamicFloat floatSource,
+ @NonNull DynamicTypeValueReceiver<Float> consumer,
+ @NonNull List<DynamicDataNode<?>> resultBuilder) {
+ DynamicDataNode<?> node;
+
+ switch (floatSource.getInnerCase()) {
+ case FIXED:
+ node = new FixedFloatNode(floatSource.getFixed(), consumer);
+ break;
+ case STATE_SOURCE:
+ node =
+ new StateFloatNode(
+ mStateStore, floatSource.getStateSource().getSourceKey(), consumer);
+ break;
+ case ARITHMETIC_OPERATION:
+ {
+ ArithmeticFloatNode arithmeticNode =
+ new ArithmeticFloatNode(floatSource.getArithmeticOperation(), consumer);
+ node = arithmeticNode;
+
+ bindRecursively(
+ floatSource.getArithmeticOperation().getInputLhs(),
+ arithmeticNode.getLhsIncomingCallback(),
+ resultBuilder);
+ bindRecursively(
+ floatSource.getArithmeticOperation().getInputRhs(),
+ arithmeticNode.getRhsIncomingCallback(),
+ resultBuilder);
+
+ break;
+ }
+ case INT32_TO_FLOAT_OPERATION:
+ {
+ Int32ToFloatNode toFloatNode = new Int32ToFloatNode(consumer);
+ node = toFloatNode;
+
+ bindRecursively(
+ floatSource.getInt32ToFloatOperation().getInput(),
+ toFloatNode.getIncomingCallback(),
+ resultBuilder);
+ break;
+ }
+ case CONDITIONAL_OP:
+ {
+ ConditionalOpNode<Float> conditionalNode = new ConditionalOpNode<>(consumer);
+
+ ConditionalFloatOp op = floatSource.getConditionalOp();
+ bindRecursively(
+ op.getCondition(),
+ conditionalNode.getConditionIncomingCallback(),
+ resultBuilder);
+ bindRecursively(
+ op.getValueIfTrue(),
+ conditionalNode.getTrueValueIncomingCallback(),
+ resultBuilder);
+ bindRecursively(
+ op.getValueIfFalse(),
+ conditionalNode.getFalseValueIncomingCallback(),
+ resultBuilder);
+
+ node = conditionalNode;
+ break;
+ }
+ case ANIMATABLE_FIXED:
+ {
+ if (mEnableAnimations) {
+ node =
+ new AnimatableFixedFloatNode(
+ floatSource.getAnimatableFixed(),
+ consumer,
+ mAnimationQuotaManager);
+ } else {
+ throw new IllegalStateException(
+ "Cannot translate static_animated_float; animations are disabled.");
+ }
+ break;
+ }
+ case ANIMATABLE_DYNAMIC:
+ {
+ if (mEnableAnimations) {
+ AnimatableDynamicFloat dynamicNode = floatSource.getAnimatableDynamic();
+ DynamicAnimatedFloatNode animationNode =
+ new DynamicAnimatedFloatNode(
+ consumer, dynamicNode.getSpec(), mAnimationQuotaManager);
+ node = animationNode;
+
+ bindRecursively(
+ dynamicNode.getInput(),
+ animationNode.getInputCallback(),
+ resultBuilder);
+ } else {
+ throw new IllegalStateException(
+ "Cannot translate dynamic_animated_float; animations are"
+ + " disabled.");
+ }
+ break;
+ }
+
+ case INNER_NOT_SET:
+ throw new IllegalArgumentException("DynamicFloat has no inner source set");
+ default:
+ throw new IllegalArgumentException("Unknown DynamicFloat source type");
+ }
+
+ resultBuilder.add(node);
+ }
+
+ /**
+ * Same as {@link #bind(DynamicBuilders.DynamicColor, DynamicTypeValueReceiver)}, all {@link
+ * DynamicDataNode} produced by evaluating given dynamic type are added to the given list.
+ */
+ private void bindRecursively(
+ @NonNull DynamicColor colorSource,
+ @NonNull DynamicTypeValueReceiver<Integer> consumer,
+ @NonNull List<DynamicDataNode<?>> resultBuilder) {
+ DynamicDataNode<?> node;
+
+ switch (colorSource.getInnerCase()) {
+ case FIXED:
+ node = new FixedColorNode(colorSource.getFixed(), consumer);
+ break;
+ case STATE_SOURCE:
+ node =
+ new StateColorSourceNode(
+ mStateStore, colorSource.getStateSource(), consumer);
+ break;
+ case ANIMATABLE_FIXED:
+ if (mEnableAnimations) {
+ node =
+ new AnimatableFixedColorNode(
+ colorSource.getAnimatableFixed(),
+ consumer,
+ mAnimationQuotaManager);
+ } else {
+ throw new IllegalStateException(
+ "Cannot translate animatable_fixed color; animations are disabled.");
+ }
+ break;
+ case ANIMATABLE_DYNAMIC:
+ if (mEnableAnimations) {
+ AnimatableDynamicColor dynamicNode = colorSource.getAnimatableDynamic();
+ DynamicAnimatedColorNode animationNode =
+ new DynamicAnimatedColorNode(
+ consumer, dynamicNode.getSpec(), mAnimationQuotaManager);
+ node = animationNode;
+
+ bindRecursively(
+ dynamicNode.getInput(),
+ animationNode.getInputCallback(),
+ resultBuilder);
+ } else {
+ throw new IllegalStateException(
+ "Cannot translate dynamic_animated_float; animations are disabled.");
+ }
+ break;
+ case INNER_NOT_SET:
+ throw new IllegalArgumentException("DynamicColor has no inner source set");
+ default:
+ throw new IllegalArgumentException("Unknown DynamicColor source type");
+ }
+
+ resultBuilder.add(node);
+ }
+
+ /**
+ * Same as {@link #bind(DynamicBuilders.DynamicBool, DynamicTypeValueReceiver)}, all {@link
+ * DynamicDataNode} produced by evaluating given dynamic type are added to the given list.
+ */
+ private void bindRecursively(
+ @NonNull DynamicBool boolSource,
+ @NonNull DynamicTypeValueReceiver<Boolean> consumer,
+ @NonNull List<DynamicDataNode<?>> resultBuilder) {
+ DynamicDataNode<?> node;
+
+ switch (boolSource.getInnerCase()) {
+ case FIXED:
+ node = new FixedBoolNode(boolSource.getFixed(), consumer);
+ break;
+ case STATE_SOURCE:
+ node = new StateBoolNode(mStateStore, boolSource.getStateSource(), consumer);
+ break;
+ case INT32_COMPARISON:
+ {
+ ComparisonInt32Node compNode =
+ new ComparisonInt32Node(boolSource.getInt32Comparison(), consumer);
+ node = compNode;
+
+ bindRecursively(
+ boolSource.getInt32Comparison().getInputLhs(),
+ compNode.getLhsIncomingCallback(),
+ resultBuilder);
+ bindRecursively(
+ boolSource.getInt32Comparison().getInputRhs(),
+ compNode.getRhsIncomingCallback(),
+ resultBuilder);
+
+ break;
+ }
+ case LOGICAL_OP:
+ {
+ LogicalBoolOp logicalNode =
+ new LogicalBoolOp(boolSource.getLogicalOp(), consumer);
+ node = logicalNode;
+
+ bindRecursively(
+ boolSource.getLogicalOp().getInputLhs(),
+ logicalNode.getLhsIncomingCallback(),
+ resultBuilder);
+ bindRecursively(
+ boolSource.getLogicalOp().getInputRhs(),
+ logicalNode.getRhsIncomingCallback(),
+ resultBuilder);
+
+ break;
+ }
+ case NOT_OP:
+ {
+ NotBoolOp notNode = new NotBoolOp(consumer);
+ node = notNode;
+ bindRecursively(
+ boolSource.getNotOp().getInput(),
+ notNode.getIncomingCallback(),
+ resultBuilder);
+ break;
+ }
+ case FLOAT_COMPARISON:
+ {
+ ComparisonFloatNode compNode =
+ new ComparisonFloatNode(boolSource.getFloatComparison(), consumer);
+ node = compNode;
+
+ bindRecursively(
+ boolSource.getFloatComparison().getInputLhs(),
+ compNode.getLhsIncomingCallback(),
+ resultBuilder);
+ bindRecursively(
+ boolSource.getFloatComparison().getInputRhs(),
+ compNode.getRhsIncomingCallback(),
+ resultBuilder);
+
+ break;
+ }
+ case INNER_NOT_SET:
+ throw new IllegalArgumentException("DynamicBool has no inner source set");
+ default:
+ throw new IllegalArgumentException("Unknown DynamicBool source type");
+ }
+
+ resultBuilder.add(node);
+ }
+
+ /** Enables sending updates on sensor and time. */
+ @UiThread
+ public void enablePlatformDataSources() {
+ if (mSensorGateway != null) {
+ mSensorGateway.enableUpdates();
+ }
+
+ mTimeGateway.enableUpdates();
+ }
+
+ /** Disables sending updates on sensor and time. */
+ @UiThread
+ public void disablePlatformDataSources() {
+ if (mSensorGateway != null) {
+ mSensorGateway.disableUpdates();
+ }
+
+ mTimeGateway.disableUpdates();
+ }
+
+ /**
+ * Closes existing time gateway.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public void close() {
+ try {
+ mTimeGateway.close();
+ } catch (RuntimeException ex) {
+ Log.e(TAG, "Error while cleaning up time gateway", ex);
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeValueReceiver.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeValueReceiver.java
new file mode 100644
index 0000000..f421d27
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeValueReceiver.java
@@ -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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.UiThread;
+
+/**
+ * Callback for an evaluation result. This is intended to support two-step updates; first a
+ * notification will be sent that the evaluation result item will be updated, then the new
+ * evaluation result will be delivered. This allows downstream consumers to properly synchronize
+ * their updates if they depend on two or more evaluation result items, rather than updating
+ * multiple times (with potentially invalid states).
+ *
+ * <p>It is guaranteed that for any given batch evaluation result, {@link #onPreUpdate()} will be
+ * called on all listeners before any {@link #onData} calls are fired.
+ *
+ * @param <T> Data type.
+ */
+public interface DynamicTypeValueReceiver<T> {
+ /**
+ * Called when evaluation result for the expression that this callback was registered for is
+ * about to be updated. This allows a downstream consumer to properly synchronize updates if it
+ * depends on two or more evaluation result items. In that case, it should use this call to
+ * figure out how many of its dependencies are going to be updated, and wait for all of them to
+ * be updated (via {@link DynamicTypeValueReceiver#onData(T)}) before acting on the change.
+ *
+ * @hide
+ */
+ @UiThread
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ void onPreUpdate();
+
+ /**
+ * Called when the expression that this callback was registered for has a new evaluation result.
+ *
+ * @see DynamicTypeValueReceiver#onPreUpdate()
+ */
+ @UiThread
+ void onData(@NonNull T newData);
+
+ /** Called when the expression that this callback was registered for has an invalid result. */
+ @UiThread
+ void onInvalidated();
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java
new file mode 100644
index 0000000..73727de
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java
@@ -0,0 +1,240 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import static androidx.wear.protolayout.expression.pipeline.AnimationsHelper.applyAnimationSpecToAnimator;
+
+import android.animation.ValueAnimator;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
+import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableFixedFloat;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ArithmeticFloatOp;
+import androidx.wear.protolayout.expression.proto.FixedProto.FixedFloat;
+
+/** Dynamic data nodes which yield floats. */
+class FloatNodes {
+
+ private FloatNodes() {}
+
+ /** Dynamic float node that has a fixed value. */
+ static class FixedFloatNode implements DynamicDataSourceNode<Float> {
+ private final float mValue;
+ private final DynamicTypeValueReceiver<Float> mDownstream;
+
+ FixedFloatNode(FixedFloat protoNode, DynamicTypeValueReceiver<Float> downstream) {
+ this.mValue = protoNode.getValue();
+ this.mDownstream = downstream;
+ }
+
+ @Override
+ @UiThread
+ public void preInit() {
+ mDownstream.onPreUpdate();
+ }
+
+ @Override
+ @UiThread
+ public void init() {
+ mDownstream.onData(mValue);
+ }
+
+ @Override
+ @UiThread
+ public void destroy() {}
+ }
+
+ /** Dynamic float node that gets value from the state. */
+ static class StateFloatNode extends StateSourceNode<Float> {
+ StateFloatNode(
+ ObservableStateStore observableStateStore,
+ String bindKey,
+ DynamicTypeValueReceiver<Float> downstream) {
+ super(observableStateStore, bindKey, se -> se.getFloatVal().getValue(), downstream);
+ }
+ }
+
+ /** Dynamic float node that supports arithmetic operations. */
+ static class ArithmeticFloatNode extends DynamicDataBiTransformNode<Float, Float, Float> {
+ private static final String TAG = "ArithmeticFloatNode";
+
+ ArithmeticFloatNode(
+ ArithmeticFloatOp protoNode, DynamicTypeValueReceiver<Float> downstream) {
+ super(
+ downstream,
+ (lhs, rhs) -> {
+ try {
+ switch (protoNode.getOperationType()) {
+ case ARITHMETIC_OP_TYPE_UNDEFINED:
+ case UNRECOGNIZED:
+ Log.e(TAG, "Unknown operation type in ArithmeticFloatNode");
+ return Float.NaN;
+ case ARITHMETIC_OP_TYPE_ADD:
+ return lhs + rhs;
+ case ARITHMETIC_OP_TYPE_SUBTRACT:
+ return lhs - rhs;
+ case ARITHMETIC_OP_TYPE_MULTIPLY:
+ return lhs * rhs;
+ case ARITHMETIC_OP_TYPE_DIVIDE:
+ return lhs / rhs;
+ case ARITHMETIC_OP_TYPE_MODULO:
+ return lhs % rhs;
+ }
+ } catch (ArithmeticException ex) {
+ Log.e(TAG, "ArithmeticException in ArithmeticFloatNode", ex);
+ return Float.NaN;
+ }
+
+ Log.e(TAG, "Unknown operation type in ArithmeticFloatNode");
+ return Float.NaN;
+ });
+ }
+ }
+
+ /** Dynamic float node that gets value from INTEGER. */
+ static class Int32ToFloatNode extends DynamicDataTransformNode<Integer, Float> {
+
+ Int32ToFloatNode(DynamicTypeValueReceiver<Float> downstream) {
+ super(downstream, i -> (float) i);
+ }
+ }
+
+ /** Dynamic float node that gets animatable value from fixed source. */
+ static class AnimatableFixedFloatNode extends AnimatableNode
+ implements DynamicDataSourceNode<Float> {
+
+ private final AnimatableFixedFloat mProtoNode;
+ private final DynamicTypeValueReceiver<Float> mDownstream;
+
+ AnimatableFixedFloatNode(
+ AnimatableFixedFloat protoNode,
+ DynamicTypeValueReceiver<Float> downstream,
+ QuotaManager quotaManager) {
+ super(quotaManager);
+ this.mProtoNode = protoNode;
+ this.mDownstream = downstream;
+ }
+
+ @Override
+ @UiThread
+ public void preInit() {
+ mDownstream.onPreUpdate();
+ }
+
+ @Override
+ @UiThread
+ public void init() {
+ ValueAnimator animator =
+ ValueAnimator.ofFloat(mProtoNode.getFromValue(), mProtoNode.getToValue());
+ animator.addUpdateListener(a -> mDownstream.onData((float) a.getAnimatedValue()));
+
+ applyAnimationSpecToAnimator(animator, mProtoNode.getSpec());
+
+ mQuotaAwareAnimator.updateAnimator(animator);
+ startOrSkipAnimator();
+ }
+
+ @Override
+ @UiThread
+ public void destroy() {
+ mQuotaAwareAnimator.stopAnimator();
+ }
+ }
+
+ /** Dynamic float node that gets animatable value from dynamic source. */
+ static class DynamicAnimatedFloatNode extends AnimatableNode implements DynamicDataNode<Float> {
+
+ final DynamicTypeValueReceiver<Float> mDownstream;
+ private final DynamicTypeValueReceiver<Float> mInputCallback;
+
+ @Nullable Float mCurrentValue = null;
+ int mPendingCalls = 0;
+
+ // Static analysis complains about calling methods of parent class AnimatableNode under
+ // initialization but mInputCallback is only used after the constructor is finished.
+ @SuppressWarnings("method.invocation.invalid")
+ DynamicAnimatedFloatNode(
+ DynamicTypeValueReceiver<Float> downstream,
+ @NonNull AnimationSpec spec,
+ QuotaManager quotaManager) {
+ super(quotaManager);
+ this.mDownstream = downstream;
+ this.mInputCallback =
+ new DynamicTypeValueReceiver<Float>() {
+ @Override
+ public void onPreUpdate() {
+ mPendingCalls++;
+
+ if (mPendingCalls == 1) {
+ mDownstream.onPreUpdate();
+
+ mQuotaAwareAnimator.resetAnimator();
+ }
+ }
+
+ @Override
+ public void onData(@NonNull Float newData) {
+ if (mPendingCalls > 0) {
+ mPendingCalls--;
+ }
+
+ if (mPendingCalls == 0) {
+ if (mCurrentValue == null) {
+ mCurrentValue = newData;
+ mDownstream.onData(mCurrentValue);
+ } else {
+ ValueAnimator animator =
+ ValueAnimator.ofFloat(mCurrentValue, newData);
+
+ applyAnimationSpecToAnimator(animator, spec);
+
+ animator.addUpdateListener(
+ a -> {
+ if (mPendingCalls == 0) {
+ mCurrentValue = (float) a.getAnimatedValue();
+ mDownstream.onData(mCurrentValue);
+ }
+ });
+
+ mQuotaAwareAnimator.updateAnimator(animator);
+ startOrSkipAnimator();
+ }
+ }
+ }
+
+ @Override
+ public void onInvalidated() {
+ if (mPendingCalls > 0) {
+ mPendingCalls--;
+ }
+
+ if (mPendingCalls == 0) {
+ mCurrentValue = null;
+ mDownstream.onInvalidated();
+ }
+ }
+ };
+ }
+
+ public DynamicTypeValueReceiver<Float> getInputCallback() {
+ return mInputCallback;
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
new file mode 100644
index 0000000..53a7968
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
@@ -0,0 +1,206 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.expression.pipeline.PlatformDataSources.EpochTimePlatformDataSource;
+import androidx.wear.protolayout.expression.pipeline.PlatformDataSources.PlatformDataSource;
+import androidx.wear.protolayout.expression.pipeline.PlatformDataSources.SensorGatewayPlatformDataSource;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ArithmeticInt32Op;
+import androidx.wear.protolayout.expression.proto.DynamicProto.FloatToInt32Op;
+import androidx.wear.protolayout.expression.proto.DynamicProto.PlatformInt32Source;
+import androidx.wear.protolayout.expression.proto.DynamicProto.PlatformInt32SourceType;
+import androidx.wear.protolayout.expression.proto.DynamicProto.StateInt32Source;
+import androidx.wear.protolayout.expression.proto.FixedProto.FixedInt32;
+
+/** Dynamic data nodes which yield integers. */
+class Int32Nodes {
+ private Int32Nodes() {}
+
+ /** Dynamic integer node that has a fixed value. */
+ static class FixedInt32Node implements DynamicDataSourceNode<Integer> {
+ private final int mValue;
+ private final DynamicTypeValueReceiver<Integer> mDownstream;
+
+ FixedInt32Node(FixedInt32 protoNode, DynamicTypeValueReceiver<Integer> downstream) {
+ this.mValue = protoNode.getValue();
+ this.mDownstream = downstream;
+ }
+
+ @Override
+ @UiThread
+ public void preInit() {
+ mDownstream.onPreUpdate();
+ }
+
+ @Override
+ @UiThread
+ public void init() {
+ mDownstream.onData(mValue);
+ }
+
+ @Override
+ @UiThread
+ public void destroy() {}
+ }
+
+ /** Dynamic integer node that gets value from the platform source. */
+ static class PlatformInt32SourceNode implements DynamicDataSourceNode<Integer> {
+ private static final String TAG = "PlatformInt32SourceNode";
+
+ @Nullable private final SensorGatewayPlatformDataSource mSensorGatewaySource;
+ @Nullable private final EpochTimePlatformDataSource mEpochTimePlatformDataSource;
+ private final PlatformInt32Source mProtoNode;
+ private final DynamicTypeValueReceiver<Integer> mDownstream;
+
+ PlatformInt32SourceNode(
+ PlatformInt32Source protoNode,
+ @Nullable EpochTimePlatformDataSource epochTimePlatformDataSource,
+ @Nullable SensorGatewayPlatformDataSource sensorGatewaySource,
+ DynamicTypeValueReceiver<Integer> downstream) {
+ this.mProtoNode = protoNode;
+ this.mEpochTimePlatformDataSource = epochTimePlatformDataSource;
+ this.mSensorGatewaySource = sensorGatewaySource;
+ this.mDownstream = downstream;
+ }
+
+ @Override
+ @UiThread
+ public void preInit() {
+ if (platformInt32SourceTypeToPlatformDataSource(mProtoNode.getSourceType()) != null) {
+ mDownstream.onPreUpdate();
+ }
+ }
+
+ @Override
+ @UiThread
+ public void init() {
+ PlatformDataSource dataSource =
+ platformInt32SourceTypeToPlatformDataSource(mProtoNode.getSourceType());
+ if (dataSource != null) {
+ dataSource.registerForData(mProtoNode.getSourceType(), mDownstream);
+ } else {
+ mDownstream.onInvalidated();
+ }
+ }
+
+ @Override
+ @UiThread
+ public void destroy() {
+ PlatformDataSource dataSource =
+ platformInt32SourceTypeToPlatformDataSource(mProtoNode.getSourceType());
+ if (dataSource != null) {
+ dataSource.unregisterForData(mProtoNode.getSourceType(), mDownstream);
+ }
+ }
+
+ @Nullable
+ private PlatformDataSource platformInt32SourceTypeToPlatformDataSource(
+ PlatformInt32SourceType sourceType) {
+ switch (sourceType) {
+ case UNRECOGNIZED:
+ case PLATFORM_INT32_SOURCE_TYPE_UNDEFINED:
+ Log.w(TAG, "Unknown PlatformInt32SourceType");
+ return null;
+ case PLATFORM_INT32_SOURCE_TYPE_CURRENT_HEART_RATE:
+ case PLATFORM_INT32_SOURCE_TYPE_DAILY_STEP_COUNT:
+ return mSensorGatewaySource;
+ case PLATFORM_INT32_SOURCE_TYPE_EPOCH_TIME_SECONDS:
+ return mEpochTimePlatformDataSource;
+ }
+ Log.w(TAG, "Unknown PlatformInt32SourceType");
+ return null;
+ }
+ }
+
+ /** Dynamic integer node that supports arithmetic operations. */
+ static class ArithmeticInt32Node extends DynamicDataBiTransformNode<Integer, Integer, Integer> {
+ private static final String TAG = "ArithmeticInt32Node";
+
+ ArithmeticInt32Node(
+ ArithmeticInt32Op protoNode, DynamicTypeValueReceiver<Integer> downstream) {
+ super(
+ downstream,
+ (lhs, rhs) -> {
+ try {
+ switch (protoNode.getOperationType()) {
+ case ARITHMETIC_OP_TYPE_UNDEFINED:
+ case UNRECOGNIZED:
+ Log.e(TAG, "Unknown operation type in ArithmeticInt32Node");
+ return 0;
+ case ARITHMETIC_OP_TYPE_ADD:
+ return lhs + rhs;
+ case ARITHMETIC_OP_TYPE_SUBTRACT:
+ return lhs - rhs;
+ case ARITHMETIC_OP_TYPE_MULTIPLY:
+ return lhs * rhs;
+ case ARITHMETIC_OP_TYPE_DIVIDE:
+ return lhs / rhs;
+ case ARITHMETIC_OP_TYPE_MODULO:
+ return lhs % rhs;
+ }
+ } catch (ArithmeticException ex) {
+ Log.e(TAG, "ArithmeticException in ArithmeticInt32Node", ex);
+ return 0;
+ }
+
+ Log.e(TAG, "Unknown operation type in ArithmeticInt32Node");
+ return 0;
+ });
+ }
+ }
+
+ /** Dynamic integer node that gets value from the state. */
+ static class StateInt32SourceNode extends StateSourceNode<Integer> {
+
+ StateInt32SourceNode(
+ ObservableStateStore observableStateStore,
+ StateInt32Source protoNode,
+ DynamicTypeValueReceiver<Integer> downstream) {
+ super(
+ observableStateStore,
+ protoNode.getSourceKey(),
+ se -> se.getInt32Val().getValue(),
+ downstream);
+ }
+ }
+
+ /** Dynamic integer node that gets value from float. */
+ static class FloatToInt32Node extends DynamicDataTransformNode<Float, Integer> {
+
+ FloatToInt32Node(FloatToInt32Op protoNode, DynamicTypeValueReceiver<Integer> downstream) {
+ super(
+ downstream,
+ x -> {
+ switch (protoNode.getRoundMode()) {
+ case ROUND_MODE_UNDEFINED:
+ case ROUND_MODE_FLOOR:
+ return (int) Math.floor(x);
+ case ROUND_MODE_ROUND:
+ return Math.round(x);
+ case ROUND_MODE_CEILING:
+ return (int) Math.ceil(x);
+ default:
+ throw new IllegalArgumentException("Unknown rounding mode");
+ }
+ });
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/MainThreadExecutor.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/MainThreadExecutor.java
new file mode 100644
index 0000000..600bb618
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/MainThreadExecutor.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.pipeline;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.Executor;
+
+/** Implements an Executor that runs on the main thread. */
+class MainThreadExecutor implements Executor {
+ private final Handler mHandler;
+
+ MainThreadExecutor() {
+ this(new Handler(Looper.getMainLooper()));
+ }
+
+ MainThreadExecutor(Handler handler) {
+ this.mHandler = handler;
+ }
+
+ @Override
+ public void execute(Runnable r) {
+ mHandler.post(r);
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/NumberFormatter.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/NumberFormatter.java
new file mode 100644
index 0000000..d024a9e
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/NumberFormatter.java
@@ -0,0 +1,184 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import static java.lang.Math.max;
+
+import android.icu.number.IntegerWidth;
+import android.icu.number.LocalizedNumberFormatter;
+import android.icu.number.NumberFormatter.GroupingStrategy;
+import android.icu.number.Precision;
+import android.icu.text.DecimalFormat;
+import android.icu.text.NumberFormat;
+import android.icu.util.ULocale;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.wear.protolayout.expression.proto.DynamicProto.FloatFormatOp;
+import androidx.wear.protolayout.expression.proto.DynamicProto.Int32FormatOp;
+
+/** Utility to number formatting. */
+class NumberFormatter {
+
+ Formatter mFormatter;
+ private static final int DEFAULT_MIN_INTEGER_DIGITS = 1;
+ private static final int DEFAULT_MAX_FRACTION_DIGITS = 3;
+
+ private interface Formatter {
+ String format(int value);
+
+ String format(float value);
+ }
+
+ NumberFormatter(FloatFormatOp floatFormatOp, ULocale currentLocale) {
+ int minIntegerDigits =
+ floatFormatOp.hasMinIntegerDigits()
+ ? floatFormatOp.getMinIntegerDigits()
+ : DEFAULT_MIN_INTEGER_DIGITS;
+ int maxFractionDigits =
+ max(
+ floatFormatOp.hasMaxFractionDigits()
+ ? floatFormatOp.getMaxFractionDigits()
+ : DEFAULT_MAX_FRACTION_DIGITS,
+ floatFormatOp.getMinFractionDigits());
+ mFormatter =
+ buildFormatter(
+ minIntegerDigits,
+ floatFormatOp.getMinFractionDigits(),
+ maxFractionDigits,
+ floatFormatOp.getGroupingUsed(),
+ currentLocale);
+ }
+
+ NumberFormatter(Int32FormatOp int32FormatOp, ULocale currentLocale) {
+ int minIntegerDigits =
+ int32FormatOp.hasMinIntegerDigits()
+ ? int32FormatOp.getMinIntegerDigits()
+ : DEFAULT_MIN_INTEGER_DIGITS;
+ mFormatter =
+ buildFormatter(
+ minIntegerDigits,
+ /* minFractionDigits= */ 0,
+ /* maxFractionDigits= */ 0,
+ int32FormatOp.getGroupingUsed(),
+ currentLocale);
+ }
+
+ String format(float value) {
+ return mFormatter.format(value);
+ }
+
+ String format(int value) {
+ return mFormatter.format(value);
+ }
+
+ @RequiresApi(VERSION_CODES.R)
+ private static class Api30Impl {
+ @NonNull
+ @DoNotInline
+ static String callFormatToString(LocalizedNumberFormatter mFmt, int value) {
+ return mFmt.format(value).toString();
+ }
+
+ @NonNull
+ @DoNotInline
+ static String callFormatToString(LocalizedNumberFormatter mFmt, float value) {
+ return mFmt.format(value).toString();
+ }
+
+ @NonNull
+ @DoNotInline
+ static LocalizedNumberFormatter buildLocalizedNumberFormatter(
+ int minIntegerDigits,
+ int minFractionDigits,
+ int maxFractionDigits,
+ boolean groupingUsed,
+ ULocale currentLocale) {
+ return android.icu.number.NumberFormatter.withLocale(currentLocale)
+ .grouping(groupingUsed ? GroupingStrategy.AUTO : GroupingStrategy.OFF)
+ .integerWidth(IntegerWidth.zeroFillTo(minIntegerDigits))
+ .precision(Precision.minMaxFraction(minFractionDigits, maxFractionDigits));
+ }
+ }
+
+ private static Formatter buildFormatter(
+ int minIntegerDigits,
+ int minFractionDigits,
+ int maxFractionDigits,
+ boolean groupingUsed,
+ ULocale currentLocale) {
+ if (VERSION.SDK_INT >= VERSION_CODES.R) {
+ return new Formatter() {
+ final LocalizedNumberFormatter mFmt =
+ Api30Impl.buildLocalizedNumberFormatter(
+ minIntegerDigits,
+ minFractionDigits,
+ maxFractionDigits,
+ groupingUsed,
+ currentLocale);
+
+ @Override
+ public String format(int value) {
+ return Api30Impl.callFormatToString(mFmt, value);
+ }
+
+ @Override
+ public String format(float value) {
+ return Api30Impl.callFormatToString(mFmt, value);
+ }
+ };
+
+ } else {
+ return new Formatter() {
+ final DecimalFormat mFmt =
+ buildDecimalFormat(
+ minIntegerDigits,
+ minFractionDigits,
+ maxFractionDigits,
+ groupingUsed,
+ currentLocale);
+
+ @Override
+ public String format(int value) {
+ return mFmt.format(value);
+ }
+
+ @Override
+ public String format(float value) {
+ return mFmt.format(value);
+ }
+ };
+ }
+ }
+
+ static DecimalFormat buildDecimalFormat(
+ int minIntegerDigits,
+ int minFractionDigits,
+ int maxFractionDigits,
+ boolean groupingUsed,
+ ULocale currentLocale) {
+ DecimalFormat decimalFormat = (DecimalFormat) NumberFormat.getInstance(currentLocale);
+ decimalFormat.setMinimumIntegerDigits(minIntegerDigits);
+ decimalFormat.setGroupingUsed(groupingUsed);
+ decimalFormat.setMaximumFractionDigits(maxFractionDigits);
+ decimalFormat.setMinimumFractionDigits(minFractionDigits);
+ return decimalFormat;
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ObservableStateStore.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ObservableStateStore.java
new file mode 100644
index 0000000..8155cb2
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ObservableStateStore.java
@@ -0,0 +1,121 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.UiThread;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+import androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * State storage for ProtoLayout, which also supports sending callback when data items change.
+ *
+ * <p>Note that this class is **not** thread-safe. Since ProtoLayout inflation currently happens on
+ * the main thread, and because updates will eventually affect the main thread, this whole class
+ * must only be used from the UI thread.
+ */
+public class ObservableStateStore {
+ @NonNull private final Map<String, StateEntryValue> mCurrentState = new ArrayMap<>();
+
+ @NonNull
+ private final Map<String, Set<DynamicTypeValueReceiver<StateEntryValue>>> mRegisteredCallbacks =
+ new ArrayMap<>();
+
+ public ObservableStateStore(@NonNull Map<String, StateEntryValue> initialState) {
+ mCurrentState.putAll(initialState);
+ }
+
+ /**
+ * Sets the given state into a storage. It replaces the current state with the new map and
+ * informs the registered listeners for changed values.
+ */
+ @UiThread
+ public void setStateEntryValues(@NonNull Map<String, StateEntryValue> newState) {
+ // Figure out which nodes have actually changed.
+ List<String> changedKeys = new ArrayList<>();
+ for (Entry<String, StateEntryValue> newEntry : newState.entrySet()) {
+ StateEntryValue currentEntry = mCurrentState.get(newEntry.getKey());
+ if (currentEntry == null || !currentEntry.equals(newEntry.getValue())) {
+ changedKeys.add(newEntry.getKey());
+ }
+ }
+
+ for (String key : changedKeys) {
+ for (DynamicTypeValueReceiver<StateEntryValue> callback :
+ mRegisteredCallbacks.getOrDefault(key, Collections.emptySet())) {
+ callback.onPreUpdate();
+ }
+ }
+
+ mCurrentState.clear();
+ mCurrentState.putAll(newState);
+
+ for (String key : changedKeys) {
+ for (DynamicTypeValueReceiver<StateEntryValue> callback :
+ mRegisteredCallbacks.getOrDefault(key, Collections.emptySet())) {
+ if (newState.containsKey(key)) {
+ // The keys come from newState, so this should never be null.
+ callback.onData(newState.get(key));
+ }
+ }
+ }
+ }
+
+ /**
+ * Gets state with the given key.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @UiThread
+ @Nullable
+ public StateEntryValue getStateEntryValues(@NonNull String key) {
+ return mCurrentState.get(key);
+ }
+
+ /**
+ * Registers the given callback for updates to the state for the given key.
+ *
+ * <p>Note that the callback will be executed on the UI thread.
+ */
+ @UiThread
+ void registerCallback(
+ @NonNull String key, @NonNull DynamicTypeValueReceiver<StateEntryValue> callback) {
+ new MainThreadExecutor();
+ mRegisteredCallbacks.computeIfAbsent(key, k -> new ArraySet<>()).add(callback);
+ }
+
+ /** Unregisters from receiving the updates. */
+ @UiThread
+ void unregisterCallback(
+ @NonNull String key, @NonNull DynamicTypeValueReceiver<StateEntryValue> callback) {
+ Set<DynamicTypeValueReceiver<StateEntryValue>> callbackSet = mRegisteredCallbacks.get(key);
+ if (callbackSet != null) {
+ callbackSet.remove(callback);
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/PlatformDataSources.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/PlatformDataSources.java
new file mode 100644
index 0000000..d2c2a6b
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/PlatformDataSources.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.pipeline;
+
+import androidx.collection.ArrayMap;
+import androidx.collection.SimpleArrayMap;
+import androidx.wear.protolayout.expression.pipeline.TimeGateway.TimeCallback;
+import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway;
+import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway.SensorDataType;
+import androidx.wear.protolayout.expression.proto.DynamicProto.PlatformInt32SourceType;
+
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/** Utility for various platform data sources. */
+class PlatformDataSources {
+ private PlatformDataSources() {}
+
+ interface PlatformDataSource {
+ void registerForData(
+ PlatformInt32SourceType sourceType, DynamicTypeValueReceiver<Integer> consumer);
+
+ void unregisterForData(
+ PlatformInt32SourceType sourceType, DynamicTypeValueReceiver<Integer> consumer);
+ }
+
+ /** Utility for time data source. */
+ static class EpochTimePlatformDataSource implements PlatformDataSource {
+ private final Executor mUiExecutor;
+ private final TimeGateway mGateway;
+ private final SimpleArrayMap<DynamicTypeValueReceiver<Integer>, TimeCallback>
+ mConsumerToTimeCallback = new SimpleArrayMap<>();
+
+ EpochTimePlatformDataSource(Executor uiExecutor, TimeGateway gateway) {
+ mUiExecutor = uiExecutor;
+ mGateway = gateway;
+ }
+
+ @Override
+ public void registerForData(
+ PlatformInt32SourceType sourceType, DynamicTypeValueReceiver<Integer> consumer) {
+ TimeCallback timeCallback =
+ new TimeCallback() {
+ @Override
+ public void onPreUpdate() {
+ consumer.onPreUpdate();
+ }
+
+ @Override
+ public void onData() {
+ long currentEpochTimeSeconds = System.currentTimeMillis() / 1000;
+ consumer.onData((int) currentEpochTimeSeconds);
+ }
+ };
+ mGateway.registerForUpdates(mUiExecutor, timeCallback);
+ mConsumerToTimeCallback.put(consumer, timeCallback);
+ }
+
+ @Override
+ public void unregisterForData(
+ PlatformInt32SourceType sourceType, DynamicTypeValueReceiver<Integer> consumer) {
+ TimeCallback timeCallback = mConsumerToTimeCallback.remove(consumer);
+ if (timeCallback != null) {
+ mGateway.unregisterForUpdates(timeCallback);
+ }
+ }
+ }
+
+ /** Utility for sensor data source. */
+ static class SensorGatewayPlatformDataSource implements PlatformDataSource {
+ private static final String TAG = "SensorGtwPltDataSource";
+ final Executor mUiExecutor;
+ private final SensorGateway mSensorGateway;
+ private final Map<DynamicTypeValueReceiver<Integer>, SensorGateway.Consumer>
+ mCallbackToRegisteredSensorConsumer = new ArrayMap<>();
+
+ SensorGatewayPlatformDataSource(Executor uiExecutor, SensorGateway sensorGateway) {
+ this.mUiExecutor = uiExecutor;
+ this.mSensorGateway = sensorGateway;
+ }
+
+ @SensorDataType
+ private int mapSensorPlatformSource(PlatformInt32SourceType platformSource) {
+ switch (platformSource) {
+ case PLATFORM_INT32_SOURCE_TYPE_CURRENT_HEART_RATE:
+ return SensorGateway.SENSOR_DATA_TYPE_HEART_RATE;
+ case PLATFORM_INT32_SOURCE_TYPE_DAILY_STEP_COUNT:
+ return SensorGateway.SENSOR_DATA_TYPE_DAILY_STEP_COUNT;
+ default:
+ throw new IllegalArgumentException("Unknown PlatformSourceType");
+ }
+ }
+
+ @Override
+ @SuppressWarnings("ExecutorTaskName")
+ public void registerForData(
+ PlatformInt32SourceType sourceType, DynamicTypeValueReceiver<Integer> callback) {
+ @SensorDataType int sensorDataType = mapSensorPlatformSource(sourceType);
+ SensorGateway.Consumer sensorConsumer =
+ new SensorGateway.Consumer() {
+ @Override
+ public void onData(double value) {
+ mUiExecutor.execute(() -> callback.onData((int) value));
+ }
+
+ @Override
+ @SensorDataType
+ public int getRequestedDataType() {
+ return sensorDataType;
+ }
+ };
+ mCallbackToRegisteredSensorConsumer.put(callback, sensorConsumer);
+ mSensorGateway.registerSensorGatewayConsumer(sensorConsumer);
+ }
+
+ @Override
+ public void unregisterForData(
+ PlatformInt32SourceType sourceType, DynamicTypeValueReceiver<Integer> consumer) {
+ SensorGateway.Consumer sensorConsumer =
+ mCallbackToRegisteredSensorConsumer.get(consumer);
+ if (sensorConsumer != null) {
+ mSensorGateway.unregisterSensorGatewayConsumer(sensorConsumer);
+ }
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/QuotaAwareAnimator.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/QuotaAwareAnimator.java
new file mode 100644
index 0000000..413b885
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/QuotaAwareAnimator.java
@@ -0,0 +1,206 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+/**
+ * Wrapper for Animator that is aware of quota. Animator's animations will be played only if given
+ * quota manager allows. If not, non infinite animation will jump to an end.
+ */
+class QuotaAwareAnimator {
+ @Nullable private ValueAnimator mAnimator;
+
+ @NonNull private final QuotaManager mQuotaManager;
+ @NonNull private final QuotaReleasingAnimatorListener mListener;
+
+ QuotaAwareAnimator(@Nullable ValueAnimator animator, @NonNull QuotaManager quotaManager) {
+ this.mAnimator = animator;
+ this.mQuotaManager = quotaManager;
+ this.mListener = new QuotaReleasingAnimatorListener(quotaManager);
+
+ if (this.mAnimator != null) {
+ this.mAnimator.addListener(mListener);
+ }
+ }
+
+ /**
+ * Sets the new animator with {link @QuotaReleasingListener} added. Previous animator will be
+ * canceled.
+ */
+ void updateAnimator(@NonNull ValueAnimator animator) {
+ cancelAnimator();
+
+ this.mAnimator = animator;
+ this.mAnimator.addListener(mListener);
+ }
+
+ /** Resets the animator to null. Previous animator will be canceled. */
+ void resetAnimator() {
+ cancelAnimator();
+
+ mAnimator = null;
+ }
+
+ /**
+ * Tries to start animation. Returns true if quota allows the animator to start. Otherwise, it
+ * returns false.
+ */
+ @UiThread
+ boolean tryStartAnimation() {
+ if (mAnimator == null) {
+ return false;
+ }
+ ValueAnimator localAnimator = mAnimator;
+ if (mQuotaManager.tryAcquireQuota(1)) {
+ startAnimator(localAnimator);
+ return true;
+ } else {
+ if (!isInfiniteAnimator()) {
+ localAnimator.end();
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Tries to start/resume infinite animation. Returns true if quota allows the animator to
+ * start/resume. Otherwise, it returns false.
+ */
+ @UiThread
+ boolean tryStartOrResumeAnimator() {
+ if (mAnimator == null) {
+ return false;
+ }
+ ValueAnimator localAnimator = mAnimator;
+ if (localAnimator.isPaused() && mQuotaManager.tryAcquireQuota(1)) {
+ resumeAnimator(localAnimator);
+ } else if (isInfiniteAnimator() && mQuotaManager.tryAcquireQuota(1)) {
+ // Infinite animators created when this node was invisible have not started yet.
+ startAnimator(localAnimator);
+ }
+ // No need to jump to an end of animation if it can't be played as they are infinite.
+ return false;
+ }
+
+ private void resumeAnimator(ValueAnimator localAnimator) {
+ localAnimator.resume();
+ mListener.mIsUsingQuota = true;
+ }
+
+ private void startAnimator(ValueAnimator localAnimator) {
+ localAnimator.start();
+ mListener.mIsUsingQuota = true;
+ }
+
+ /**
+ * Stops or pauses the animator, depending on it's state. If stopped, it will assign the end
+ * value.
+ */
+ @UiThread
+ void stopOrPauseAnimator() {
+ if (mAnimator == null) {
+ return;
+ }
+ ValueAnimator localAnimator = mAnimator;
+ if (isInfiniteAnimator()) {
+ localAnimator.pause();
+ } else {
+ // This causes the animation to assign the end value of the property being animated.
+ stopAnimator();
+ }
+ }
+
+ /** Stops the animator, which will cause it to assign the end value. */
+ @UiThread
+ void stopAnimator() {
+ if (mAnimator == null) {
+ return;
+ }
+ mAnimator.end();
+ }
+
+ /** Cancels the animator, which will stop in its tracks. */
+ @UiThread
+ void cancelAnimator() {
+ if (mAnimator == null) {
+ return;
+ }
+ // This calls both onCancel and onEnd methods from listener.
+ mAnimator.cancel();
+ mAnimator.removeListener(mListener);
+ }
+
+ /** Returns whether the animator in this class has an infinite duration. */
+ protected boolean isInfiniteAnimator() {
+ return mAnimator != null && mAnimator.getTotalDuration() == Animator.DURATION_INFINITE;
+ }
+
+ /** Returns whether this node has a running animation. */
+ boolean hasRunningAnimation() {
+ return mAnimator != null && mAnimator.isRunning();
+ }
+
+ /**
+ * The listener used for animatable nodes to release quota when the animation is finished or
+ * paused.
+ */
+ private static final class QuotaReleasingAnimatorListener extends AnimatorListenerAdapter {
+ @NonNull private final QuotaManager mQuotaManager;
+
+ // We need to keep track of whether the animation has started because pipeline has initiated
+ // and it has received quota, or onAnimationStart listener has been called because of the
+ // inner ValueAnimator implementation (i.e., when calling end() on animator to assign it end
+ // value, ValueAnimator will call start first if animation is not running to get it to the
+ // end state.
+ boolean mIsUsingQuota = false;
+
+ QuotaReleasingAnimatorListener(@NonNull QuotaManager quotaManager) {
+ this.mQuotaManager = quotaManager;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {}
+
+ @Override
+ public void onAnimationResume(Animator animation) {}
+
+ @Override
+ @UiThread
+ public void onAnimationEnd(Animator animation) {
+ if (mIsUsingQuota) {
+ mQuotaManager.releaseQuota(1);
+ mIsUsingQuota = false;
+ }
+ }
+
+ @Override
+ @UiThread
+ public void onAnimationPause(Animator animation) {
+ if (mIsUsingQuota) {
+ mQuotaManager.releaseQuota(1);
+ mIsUsingQuota = false;
+ }
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/QuotaManager.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/QuotaManager.java
new file mode 100644
index 0000000..fa8d642
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/QuotaManager.java
@@ -0,0 +1,54 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+
+/**
+ * Interface responsible for managing quota. Before initiating some action (e.g. starting an
+ * animation) that uses a limited resource, {@link #tryAcquireQuota} should be called to see if the
+ * target quota cap has been reached and if not, it will be updated. When action/resource is
+ * finished, it should be release with {@link #releaseQuota}.
+ *
+ * <p>For example, this can be used for limiting the number of concurrently running animations.
+ * Every time new animations is due to be played, it should request quota from {@link QuotaManager}
+ * in amount that is equal to the number of animations that should be played (e.g. playing fade in
+ * and slide in animation on one object should request amount of 2 quota.
+ *
+ * <p>It is callers responsibility to release acquired quota after limited resource has been
+ * finished. For example, when animation is running, but surface became invisible, caller should
+ * return acquired quota.
+ */
+public interface QuotaManager {
+
+ /**
+ * Tries to acquire the given amount of quota and returns true if successful. Otherwise, returns
+ * false meaning that quota cap has already been reached and the quota won't be acquired.
+ *
+ * <p>It is callers responsibility to release acquired quota after limited resource has been
+ * finished. For example, when animation is running, but surface became invisible, caller should
+ * return acquired quota.
+ */
+ boolean tryAcquireQuota(int quota);
+
+ /**
+ * Releases the given amount of quota.
+ *
+ * @throws IllegalArgumentException if the given quota amount exceeds the amount of acquired
+ * quota.
+ */
+ void releaseQuota(int quota);
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java
new file mode 100644
index 0000000..8d14ff8
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java
@@ -0,0 +1,83 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue;
+
+import java.util.function.Function;
+
+class StateSourceNode<T>
+ implements DynamicDataSourceNode<T>, DynamicTypeValueReceiver<StateEntryValue> {
+ private final ObservableStateStore mObservableStateStore;
+ private final String mBindKey;
+ private final Function<StateEntryValue, T> mStateExtractor;
+ private final DynamicTypeValueReceiver<T> mDownstream;
+
+ StateSourceNode(
+ ObservableStateStore observableStateStore,
+ String bindKey,
+ Function<StateEntryValue, T> stateExtractor,
+ DynamicTypeValueReceiver<T> downstream) {
+ this.mObservableStateStore = observableStateStore;
+ this.mBindKey = bindKey;
+ this.mStateExtractor = stateExtractor;
+ this.mDownstream = downstream;
+ }
+
+ @Override
+ @UiThread
+ public void preInit() {
+ mDownstream.onPreUpdate();
+ }
+
+ @Override
+ @UiThread
+ public void init() {
+ mObservableStateStore.registerCallback(mBindKey, this);
+ StateEntryValue item = mObservableStateStore.getStateEntryValues(mBindKey);
+
+ if (item != null) {
+ this.onData(item);
+ } else {
+ this.onInvalidated();
+ }
+ }
+
+ @Override
+ @UiThread
+ public void destroy() {
+ mObservableStateStore.unregisterCallback(mBindKey, this);
+ }
+
+ @Override
+ public void onPreUpdate() {
+ mDownstream.onPreUpdate();
+ }
+
+ @Override
+ public void onData(@Nullable StateEntryValue newData) {
+ T actualValue = mStateExtractor.apply(newData);
+ mDownstream.onData(actualValue);
+ }
+
+ @Override
+ public void onInvalidated() {
+ mDownstream.onInvalidated();
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StringNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StringNodes.java
new file mode 100644
index 0000000..1c224d4
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StringNodes.java
@@ -0,0 +1,89 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.expression.proto.DynamicProto.StateStringSource;
+import androidx.wear.protolayout.expression.proto.FixedProto.FixedString;
+
+/** Dynamic data nodes which yield Strings. */
+class StringNodes {
+ private StringNodes() {}
+
+ /** Dynamic string node that has a fixed value. */
+ static class FixedStringNode implements DynamicDataSourceNode<String> {
+ private final String mValue;
+ private final DynamicTypeValueReceiver<String> mDownstream;
+
+ FixedStringNode(FixedString protoNode, DynamicTypeValueReceiver<String> downstream) {
+ this.mValue = protoNode.getValue();
+ this.mDownstream = downstream;
+ }
+
+ @Override
+ @UiThread
+ public void preInit() {
+ mDownstream.onPreUpdate();
+ }
+
+ @Override
+ @UiThread
+ public void init() {
+ mDownstream.onData(mValue);
+ }
+
+ @Override
+ @UiThread
+ public void destroy() {}
+ }
+
+ /** Dynamic string node that gets a value from integer. */
+ static class Int32FormatNode extends DynamicDataTransformNode<Integer, String> {
+ Int32FormatNode(NumberFormatter formatter, DynamicTypeValueReceiver<String> downstream) {
+ super(downstream, formatter::format);
+ }
+ }
+
+ /** Dynamic string node that gets a value from the other strings. */
+ static class StringConcatOpNode extends DynamicDataBiTransformNode<String, String, String> {
+ StringConcatOpNode(DynamicTypeValueReceiver<String> downstream) {
+ super(downstream, String::concat);
+ }
+ }
+
+ /** Dynamic string node that gets a value from float. */
+ static class FloatFormatNode extends DynamicDataTransformNode<Float, String> {
+
+ FloatFormatNode(NumberFormatter formatter, DynamicTypeValueReceiver<String> downstream) {
+ super(downstream, formatter::format);
+ }
+ }
+
+ /** Dynamic string node that gets a value from the state. */
+ static class StateStringNode extends StateSourceNode<String> {
+ StateStringNode(
+ ObservableStateStore observableStateStore,
+ StateStringSource protoNode,
+ DynamicTypeValueReceiver<String> downstream) {
+ super(
+ observableStateStore,
+ protoNode.getSourceKey(),
+ se -> se.getStringVal().getValue(),
+ downstream);
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGateway.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGateway.java
new file mode 100644
index 0000000..786e1a2
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGateway.java
@@ -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.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.NonNull;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A gateway to Time data sources. This should call any provided callbacks every second.
+ *
+ * <p>Implementations of this class should track a few things:
+ *
+ * <ul>
+ * <li>Surface lifecycle. Implementations should keep track of registered consumers, and
+ * deregister them all when the surface is no longer available.
+ * <li>Device state. Implementations should react to device state (i.e. ambient mode), and
+ * activity state (i.e. is the surface in the foreground), and enable/disable updates
+ * accordingly.
+ * </ul>
+ */
+interface TimeGateway {
+ /** Callback for time notifications. */
+ interface TimeCallback {
+ /**
+ * Called just before an update happens. All onPreUpdate calls will be made before any
+ * onUpdate calls fire.
+ *
+ * <p>Will be called on the same executor passed to {@link TimeGateway#registerForUpdates}.
+ */
+ void onPreUpdate();
+
+ /**
+ * Notifies that the current time has changed.
+ *
+ * <p>Will be called on the same executor passed to {@link TimeGateway#registerForUpdates}.
+ */
+ void onData();
+ }
+
+ /** Register for time updates. All callbacks will be called on the provided executor. */
+ void registerForUpdates(@NonNull Executor executor, @NonNull TimeCallback callback);
+
+ /** Unregister for time updates. */
+ void unregisterForUpdates(@NonNull TimeCallback callback);
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java
new file mode 100644
index 0000000..4c6a3e5
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java
@@ -0,0 +1,128 @@
+package androidx.wear.protolayout.expression.pipeline;
+
+import android.os.Handler;
+import android.os.SystemClock;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+import androidx.collection.ArrayMap;
+
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Default implementation of {@link TimeGateway} using Android's clock.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public class TimeGatewayImpl implements TimeGateway, AutoCloseable {
+ private final Handler uiHandler;
+ private final Map<TimeCallback, Executor> registeredCallbacks = new ArrayMap<>();
+ private boolean updatesEnabled;
+ private final Runnable onTick;
+
+ private long lastScheduleTimeMillis = 0;
+
+ // Suppress warning on "onTick = this::notifyNextSecond". This happens because notifyNextSecond
+ // is @UnderInitialization here, but onTick needs to be @Initialized. This is safe though; the
+ // only time that onTick can be invoked is in the other methods on this class, which can only be
+ // called after initialization is complete. This class is also final, so those methods cannot
+ // be called from a sub-constructor either.
+ @SuppressWarnings("methodref.receiver.bound")
+ public TimeGatewayImpl(@NonNull Handler uiHandler, boolean updatesEnabled) {
+ this.uiHandler = uiHandler;
+ this.updatesEnabled = updatesEnabled;
+
+ this.onTick = this::notifyNextSecond;
+ }
+
+ /** See {@link TimeGateway#registerForUpdates(Executor, TimeCallback)}. */
+ @Override
+ public void registerForUpdates(@NonNull Executor executor, @NonNull TimeCallback callback) {
+ registeredCallbacks.put(callback, executor);
+
+ // If this was the first registration, _and_ we're enabled, then schedule the message on the
+ // Handler (otherwise, another call has already scheduled the call).
+ if (registeredCallbacks.size() == 1 && this.updatesEnabled) {
+ lastScheduleTimeMillis = SystemClock.uptimeMillis() + 1000;
+ uiHandler.postAtTime(this.onTick, this, lastScheduleTimeMillis);
+ }
+ }
+
+ /** See {@link TimeGateway#unregisterForUpdates(TimeCallback)}. */
+ @Override
+ public void unregisterForUpdates(@NonNull TimeCallback callback) {
+ registeredCallbacks.remove(callback);
+
+ // If there are no more registered callbacks, stop the periodic call.
+ if (registeredCallbacks.isEmpty() && this.updatesEnabled) {
+ uiHandler.removeCallbacks(this.onTick, this);
+ }
+ }
+
+ @UiThread
+ public void enableUpdates() {
+ setUpdatesEnabled(true);
+ }
+
+ @UiThread
+ public void disableUpdates() {
+ setUpdatesEnabled(false);
+ }
+
+ private void setUpdatesEnabled(boolean updatesEnabled) {
+ if (updatesEnabled == this.updatesEnabled) {
+ return;
+ }
+
+ this.updatesEnabled = updatesEnabled;
+
+ if (!updatesEnabled) {
+ uiHandler.removeCallbacks(this.onTick, this);
+ } else if (!registeredCallbacks.isEmpty()) {
+ lastScheduleTimeMillis = SystemClock.uptimeMillis() + 1000;
+
+ uiHandler.postAtTime(this.onTick, this, lastScheduleTimeMillis);
+ }
+ }
+
+ @SuppressWarnings("ExecutorTaskName")
+ private void notifyNextSecond() {
+ if (!this.updatesEnabled) {
+ return;
+ }
+
+ for (Map.Entry<TimeCallback, Executor> callback : registeredCallbacks.entrySet()) {
+ callback.getValue().execute(callback.getKey()::onPreUpdate);
+ }
+
+ for (Map.Entry<TimeCallback, Executor> callback : registeredCallbacks.entrySet()) {
+ callback.getValue().execute(callback.getKey()::onData);
+ }
+
+ lastScheduleTimeMillis += 1000;
+
+ // Ensure that the new time is actually in the future. If a call from uiHandler gets
+ // significantly delayed for any reason, then without this, we'll reschedule immediately
+ // (potentially multiple times), compounding the situation further.
+ if (lastScheduleTimeMillis < SystemClock.uptimeMillis()) {
+ // Skip the failed updates...
+ long missedTime = SystemClock.uptimeMillis() - lastScheduleTimeMillis;
+
+ // Round up to the nearest second...
+ missedTime = ((missedTime / 1000) + 1) * 1000;
+ lastScheduleTimeMillis += missedTime;
+ }
+
+ uiHandler.postAtTime(this.onTick, this, lastScheduleTimeMillis);
+ }
+
+ @Override
+ public void close() {
+ setUpdatesEnabled(false);
+ registeredCallbacks.clear();
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/sensor/SensorGateway.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/sensor/SensorGateway.java
new file mode 100644
index 0000000..513f38d
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/sensor/SensorGateway.java
@@ -0,0 +1,203 @@
+/*
+ * 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.wear.protolayout.expression.pipeline.sensor;
+
+import android.Manifest;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresPermission;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+
+import java.io.Closeable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+
+/**
+ * Gateway for proto layout expression library to be able to access sensor data, e.g. health data.
+ *
+ * <p>Implementations of this class should track a few things:
+ *
+ * <ul>
+ * <li>Surface lifecycle. Implementations should keep track of the surface provider, registered
+ * consumers, and deregister them all when the surface is not longer available.
+ * <li>Device state. Implementations should react to device state (i.e. ambient mode), and
+ * activity state (i.e. surface being in the foreground), and appropriately set the sampling
+ * rate of the sensor (e.g. high rate when surface is in the foreground, otherwise low-rate or
+ * off).
+ * </ul>
+ */
+public interface SensorGateway extends Closeable {
+
+ /**
+ * Sensor data types that can be subscribed to from {@link SensorGateway}.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ SENSOR_DATA_TYPE_INVALID,
+ SENSOR_DATA_TYPE_HEART_RATE,
+ SENSOR_DATA_TYPE_DAILY_STEP_COUNT
+ })
+ public @interface SensorDataType {};
+
+ /** Invalid data type. Used to return error states. */
+ int SENSOR_DATA_TYPE_INVALID = -1;
+
+ /**
+ * The user's current heart rate. This is an instantaneous reading from the last time it was
+ * sampled. Note that this means that apps which subscribe to passive heart rate data may not
+ * receive exact heart rate data; it will be batched to a given period.
+ */
+ @RequiresPermission(Manifest.permission.BODY_SENSORS)
+ int SENSOR_DATA_TYPE_HEART_RATE = 0;
+
+ /**
+ * The user's current daily step count. Note that this data type will reset to zero at midnight.
+ * each day, and any subscriptions to this data type will log the number of steps the user has
+ * done since 12:00AM local time.
+ */
+ @RequiresPermission(Manifest.permission.ACTIVITY_RECOGNITION)
+ int SENSOR_DATA_TYPE_DAILY_STEP_COUNT = 1;
+
+ /**
+ * Consumer for sensor data.
+ *
+ * <p>If Consumer is relying on multiple sources or upstream nodes, it should be responsible for
+ * data coordination between pending updates and received data.
+ *
+ * <p>For example, this Consumer listens to two upstream sources:
+ *
+ * <pre>{@code
+ * class MyConsumer implements Consumer {
+ * int pending = 0;
+ *
+ * @Override
+ * public void onPreUpdate(){
+ * pending++;
+ * }
+ *
+ * @Override
+ * public void onData(double value){
+ * // store the value internally
+ * pending--;
+ * if (pending == 0) {
+ * // We've received data from every changed upstream source
+ * consumeTheChangedDataValues()
+ * }
+ * }
+ * }
+ * }</pre>
+ */
+ interface Consumer {
+ /**
+ * Called when a new batch of data has arrived. The actual data will be delivered in {@link
+ * #onData(double)} after this method is called on all registered consumers.
+ */
+ @AnyThread
+ default void onPreUpdate() {}
+
+ /**
+ * Called when a new data for the requested data type is received. This will be run on a
+ * single background thread.
+ *
+ * <p>Note that there is no notification when a daily sensor data item resets to zero; this
+ * will simply emit an item of sensor data with value 0.0 when the rollover happens.
+ */
+ @AnyThread
+ void onData(double value);
+
+ /**
+ * Notify that the current data for the registered data type has been invalidated. This
+ * could be, for example, that the current heart rate is no longer valid as the user is not
+ * wearing the device.
+ */
+ @AnyThread
+ default void onInvalidated() {}
+
+ /** The sensor data type to be consumed. */
+ @SensorDataType
+ int getRequestedDataType();
+ }
+
+ /**
+ * Enables/unpauses sending updates to the consumers. All cached updates (while updates were
+ * paused) for data types will be delivered by sending the latest data.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP_PREFIX)
+ void enableUpdates();
+
+ /**
+ * Disables/pauses sending updates to the consumers. While paused, updates will be cached to be
+ * delivered after unpausing.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP_PREFIX)
+ void disableUpdates();
+
+ /**
+ * Register for updates for {@link Consumer#getRequestedDataType()} data type. This may cause
+ * {@link Consumer} to immediately fire if there is suitable cached data, otherwise {@link
+ * Consumer} will fire when there is appropriate updates to the requested sensor data.
+ *
+ * <p>Implementations should check if the provider has permission to provide the requested data
+ * type.
+ *
+ * <p>Note that the callback will be executed on the single background thread (implementation
+ * dependent). To specify the execution thread, use {@link
+ * #registerSensorGatewayConsumer(Executor, Consumer)}.
+ *
+ * @throws SecurityException if the provider does not have permission to provide requested data
+ * type.
+ */
+ @UiThread
+ void registerSensorGatewayConsumer(@NonNull Consumer consumer);
+
+ /**
+ * Register for updates for {@link Consumer#getRequestedDataType()} data type. This may cause
+ * {@link Consumer} to immediately fire if there is suitable cached data, otherwise {@link
+ * Consumer} will fire when there is appropriate updates to the requested sensor data.
+ *
+ * <p>Implementations should check if the provider has permission to provide the requested data
+ * type.
+ *
+ * <p>The callback will be executed on the provided {@link Executor}.
+ *
+ * @throws SecurityException if the provider does not have permission to provide requested data
+ * type.
+ */
+ @UiThread
+ void registerSensorGatewayConsumer(
+ @NonNull /* @CallbackExecutor */ Executor executor, @NonNull Consumer consumer);
+
+ /** Unregister for updates for {@link Consumer#getRequestedDataType()} data type. */
+ @UiThread
+ void unregisterSensorGatewayConsumer(@NonNull Consumer consumer);
+
+ /** See {@link Closeable#close()}. */
+ @Override
+ void close();
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/AnimatableNodeTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/AnimatableNodeTest.java
new file mode 100644
index 0000000..e0f76fd
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/AnimatableNodeTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.pipeline;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.animation.ValueAnimator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class AnimatableNodeTest {
+
+ @Test
+ public void infiniteAnimator_onlyStartsWhenNodeIsVisible() {
+ ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 10.0f);
+ QuotaManager quotaManager = new UnlimitedQuotaManager();
+ TestQuotaAwareAnimator quotaAwareAnimator =
+ new TestQuotaAwareAnimator(animator, quotaManager);
+ TestAnimatableNode animNode = new TestAnimatableNode(quotaAwareAnimator);
+
+ quotaAwareAnimator.isInfiniteAnimator = true;
+
+ assertThat(animator.isRunning()).isFalse();
+
+ animNode.startOrSkipAnimator();
+ assertThat(animator.isRunning()).isFalse();
+
+ animNode.setVisibility(true);
+ assertThat(animator.isRunning()).isTrue();
+ }
+
+ @Test
+ public void infiniteAnimator_pausesWhenNodeIsInvisible() {
+ ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 10.0f);
+ QuotaManager quotaManager = new UnlimitedQuotaManager();
+ TestQuotaAwareAnimator quotaAwareAnimator =
+ new TestQuotaAwareAnimator(animator, quotaManager);
+ TestAnimatableNode animNode = new TestAnimatableNode(quotaAwareAnimator);
+
+ quotaAwareAnimator.isInfiniteAnimator = true;
+
+ animNode.setVisibility(true);
+ assertThat(animator.isRunning()).isTrue();
+
+ animNode.setVisibility(false);
+ assertThat(animator.isPaused()).isTrue();
+
+ animNode.setVisibility(true);
+ assertThat(animator.isRunning()).isTrue();
+ }
+
+ @Test
+ public void animator_noQuota_notPlayed() {
+ ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 10.0f);
+ QuotaManager quotaManager = new TestNoQuotaManagerImpl();
+ TestQuotaAwareAnimator quotaAwareAnimator =
+ new TestQuotaAwareAnimator(animator, quotaManager);
+ TestAnimatableNode animNode = new TestAnimatableNode(quotaAwareAnimator);
+
+ // Check that animator hasn't started because there is no quota.
+ animNode.setVisibility(true);
+ assertThat(animator.isStarted()).isFalse();
+ assertThat(animator.isRunning()).isFalse();
+ }
+
+ static class TestAnimatableNode extends AnimatableNode {
+
+ TestAnimatableNode(QuotaAwareAnimator quotaAwareAnimator) {
+ super(quotaAwareAnimator);
+ }
+ }
+
+ static class TestQuotaAwareAnimator extends QuotaAwareAnimator {
+ public boolean isInfiniteAnimator = false;
+
+ TestQuotaAwareAnimator(
+ @Nullable ValueAnimator animator, @NonNull QuotaManager mQuotaManager) {
+ super(animator, mQuotaManager);
+ }
+
+ @Override
+ protected boolean isInfiniteAnimator() {
+ return isInfiniteAnimator;
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/TestNoQuotaManagerImpl.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/TestNoQuotaManagerImpl.java
new file mode 100644
index 0000000..520e2cdc
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/TestNoQuotaManagerImpl.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.pipeline;
+
+/** QuotaManager that doesn't allow any quota. */
+public class TestNoQuotaManagerImpl implements QuotaManager {
+
+ @Override
+ public boolean tryAcquireQuota(int quota) {
+ return false;
+ }
+
+ @Override
+ public void releaseQuota(int quota) {}
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/UnlimitedQuotaManager.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/UnlimitedQuotaManager.java
new file mode 100644
index 0000000..e4c814a
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/UnlimitedQuotaManager.java
@@ -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.wear.protolayout.expression.pipeline;
+
+/** Default, unlimited quota manager implementation that always returns true. */
+public class UnlimitedQuotaManager implements QuotaManager {
+ private int mQuotaCounter = 0;
+
+ /**
+ * @see QuotaManager#tryAcquireQuota
+ * <p>Note that this method is not thread safe.
+ */
+ @Override
+ public boolean tryAcquireQuota(int quota) {
+ mQuotaCounter += quota;
+ return true;
+ }
+
+ /**
+ * @see QuotaManager#releaseQuota
+ * <p>Note that this method is not thread safe.
+ */
+ @Override
+ public void releaseQuota(int quota) {
+ mQuotaCounter -= quota;
+ }
+
+ /** Returns true if all quota has been released. */
+ public boolean isAllQuotaReleased() {
+ return mQuotaCounter == 0;
+ }
+}
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index 7b81823..d9949ab 100644
--- a/wear/protolayout/protolayout-expression/api/current.txt
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -113,10 +113,38 @@
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat animate();
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 asInt();
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat constant(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(float);
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format();
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter);
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromByteArray(byte[]);
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromState(String);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(float);
+ method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat!,java.lang.Float!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
method public default byte[] toDynamicFloatByteArray();
}
@@ -131,10 +159,43 @@
public static interface DynamicBuilders.DynamicInt32 extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat asFloat();
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 constant(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 div(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(int);
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format();
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter);
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromByteArray(byte[]);
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromState(String);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 minus(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(int);
+ method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32!,java.lang.Integer!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 plus(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 rem(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 times(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
method public default byte[] toDynamicInt32ByteArray();
}
diff --git a/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
index a8b4bed..be278c8 100644
--- a/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
@@ -113,10 +113,38 @@
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat animate();
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 asInt();
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat constant(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(float);
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format();
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter);
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromByteArray(byte[]);
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromState(String);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(float);
+ method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat!,java.lang.Float!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
method public default byte[] toDynamicFloatByteArray();
}
@@ -131,10 +159,43 @@
public static interface DynamicBuilders.DynamicInt32 extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat asFloat();
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 constant(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 div(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(int);
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format();
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter);
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromByteArray(byte[]);
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromState(String);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 minus(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(int);
+ method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32!,java.lang.Integer!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 plus(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 rem(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 times(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
method public default byte[] toDynamicInt32ByteArray();
}
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
index 7b81823..d9949ab 100644
--- a/wear/protolayout/protolayout-expression/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -113,10 +113,38 @@
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat animate();
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 asInt();
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat constant(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(float);
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format();
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter);
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromByteArray(byte[]);
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromState(String);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(float);
+ method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat!,java.lang.Float!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
method public default byte[] toDynamicFloatByteArray();
}
@@ -131,10 +159,43 @@
public static interface DynamicBuilders.DynamicInt32 extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat asFloat();
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 constant(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 div(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat div(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(int);
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format();
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter);
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromByteArray(byte[]);
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromState(String);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lt(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool lte(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 minus(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat minus(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(int);
+ method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32!,java.lang.Integer!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 plus(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat plus(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 rem(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat rem(float);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 times(int);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
method public default byte[] toDynamicInt32ByteArray();
}
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
index 1d25fe0..9b6c389 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
@@ -58,7 +58,7 @@
/**
* Incoming elements are animated using deceleration easing, which starts a transition at peak
- * velocity (the fastest point of an element’s movement) and ends at rest.
+ * velocity (the fastest point of an element's movement) and ends at rest.
*
* <p>This is equivalent to the Compose {@code LinearOutSlowInEasing}.
*/
@@ -363,7 +363,7 @@
}
/**
- * The cubic polynomial easing that implements third-order Bézier curves. This is equivalent to
+ * The cubic polynomial easing that implements third-order Bezier curves. This is equivalent to
* the Android PathInterpolator.
*
* @since 1.2
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
index e6d1dab..dfecd528 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
@@ -46,6 +46,69 @@
private DynamicBuilders() {}
/**
+ * The type of arithmetic operation used in {@link ArithmeticInt32Op} and {@link
+ * ArithmeticFloatOp}.
+ *
+ * @hide
+ * @since 1.2
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @IntDef({
+ ARITHMETIC_OP_TYPE_UNDEFINED,
+ ARITHMETIC_OP_TYPE_ADD,
+ ARITHMETIC_OP_TYPE_SUBTRACT,
+ ARITHMETIC_OP_TYPE_MULTIPLY,
+ ARITHMETIC_OP_TYPE_DIVIDE,
+ ARITHMETIC_OP_TYPE_MODULO
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ArithmeticOpType {
+
+ }
+
+ /**
+ * Undefined operation type.
+ *
+ * @since 1.2
+ */
+ static final int ARITHMETIC_OP_TYPE_UNDEFINED = 0;
+
+ /**
+ * Addition.
+ *
+ * @since 1.2
+ */
+ static final int ARITHMETIC_OP_TYPE_ADD = 1;
+
+ /**
+ * Subtraction.
+ *
+ * @since 1.2
+ */
+ static final int ARITHMETIC_OP_TYPE_SUBTRACT = 2;
+
+ /**
+ * Multiplication.
+ *
+ * @since 1.2
+ */
+ static final int ARITHMETIC_OP_TYPE_MULTIPLY = 3;
+
+ /**
+ * Division.
+ *
+ * @since 1.2
+ */
+ static final int ARITHMETIC_OP_TYPE_DIVIDE = 4;
+
+ /**
+ * Modulus.
+ *
+ * @since 1.2
+ */
+ static final int ARITHMETIC_OP_TYPE_MODULO = 5;
+
+ /**
* Rounding mode to use when converting a float to an int32.
*
* @since 1.2
@@ -92,13 +155,13 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@IntDef({
- COMPARISON_OP_TYPE_UNDEFINED,
- COMPARISON_OP_TYPE_EQUALS,
- COMPARISON_OP_TYPE_NOT_EQUALS,
- COMPARISON_OP_TYPE_LESS_THAN,
- COMPARISON_OP_TYPE_LESS_THAN_OR_EQUAL_TO,
- COMPARISON_OP_TYPE_GREATER_THAN,
- COMPARISON_OP_TYPE_GREATER_THAN_OR_EQUAL_TO
+ COMPARISON_OP_TYPE_UNDEFINED,
+ COMPARISON_OP_TYPE_EQUALS,
+ COMPARISON_OP_TYPE_NOT_EQUALS,
+ COMPARISON_OP_TYPE_LESS_THAN,
+ COMPARISON_OP_TYPE_LESS_THAN_OR_EQUAL_TO,
+ COMPARISON_OP_TYPE_GREATER_THAN,
+ COMPARISON_OP_TYPE_GREATER_THAN_OR_EQUAL_TO
})
@Retention(RetentionPolicy.SOURCE)
@interface ComparisonOpType {}
@@ -186,6 +249,150 @@
static final int LOGICAL_OP_TYPE_OR = 2;
/**
+ * An arithmetic operation, operating on two Int32 instances. This implements simple binary
+ * operations of the form "result = LHS <op> RHS", where the available operation types are
+ * described in {@code ArithmeticOpType}.
+ *
+ * @since 1.2
+ */
+ static final class ArithmeticInt32Op implements DynamicInt32 {
+
+ private final DynamicProto.ArithmeticInt32Op mImpl;
+ @Nullable
+ private final Fingerprint mFingerprint;
+
+ ArithmeticInt32Op(DynamicProto.ArithmeticInt32Op impl, @Nullable Fingerprint fingerprint) {
+ this.mImpl = impl;
+ this.mFingerprint = fingerprint;
+ }
+
+ /**
+ * Gets left hand side of the arithmetic operation.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public DynamicInt32 getInputLhs() {
+ if (mImpl.hasInputLhs()) {
+ return DynamicBuilders.dynamicInt32FromProto(mImpl.getInputLhs());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets right hand side of the arithmetic operation.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public DynamicInt32 getInputRhs() {
+ if (mImpl.hasInputRhs()) {
+ return DynamicBuilders.dynamicInt32FromProto(mImpl.getInputRhs());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the type of operation to carry out.
+ *
+ * @since 1.2
+ */
+ @ArithmeticOpType
+ public int getOperationType() {
+ return mImpl.getOperationType().getNumber();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public Fingerprint getFingerprint() {
+ return mFingerprint;
+ }
+
+ @NonNull
+ static ArithmeticInt32Op fromProto(@NonNull DynamicProto.ArithmeticInt32Op proto) {
+ return new ArithmeticInt32Op(proto, null);
+ }
+
+ @NonNull
+ DynamicProto.ArithmeticInt32Op toProto() {
+ return mImpl;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public DynamicProto.DynamicInt32 toDynamicInt32Proto() {
+ return DynamicProto.DynamicInt32.newBuilder().setArithmeticOperation(mImpl).build();
+ }
+
+ /**
+ * Builder for {@link ArithmeticInt32Op}.
+ */
+ public static final class Builder implements DynamicInt32.Builder {
+
+ private final DynamicProto.ArithmeticInt32Op.Builder mImpl =
+ DynamicProto.ArithmeticInt32Op.newBuilder();
+ private final Fingerprint mFingerprint = new Fingerprint(-2012727925);
+
+ public Builder() {
+ }
+
+ /**
+ * Sets left hand side of the arithmetic operation.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setInputLhs(@NonNull DynamicInt32 inputLhs) {
+ mImpl.setInputLhs(inputLhs.toDynamicInt32Proto());
+ mFingerprint.recordPropertyUpdate(
+ 1, checkNotNull(inputLhs.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ /**
+ * Sets right hand side of the arithmetic operation.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setInputRhs(@NonNull DynamicInt32 inputRhs) {
+ mImpl.setInputRhs(inputRhs.toDynamicInt32Proto());
+ mFingerprint.recordPropertyUpdate(
+ 2, checkNotNull(inputRhs.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ /**
+ * Sets the type of operation to carry out.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setOperationType(@ArithmeticOpType int operationType) {
+ mImpl.setOperationType(DynamicProto.ArithmeticOpType.forNumber(operationType));
+ mFingerprint.recordPropertyUpdate(3, operationType);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public ArithmeticInt32Op build() {
+ return new ArithmeticInt32Op(mImpl.build(), mFingerprint);
+ }
+ }
+ }
+
+ /**
* A dynamic Int32 which sources its data from the tile's state.
*
* @since 1.2
@@ -200,7 +407,7 @@
}
/**
- * Gets the key in the state to bind to. Intended for testing purposes only.
+ * Gets the key in the state to bind to.
*
* @since 1.2
*/
@@ -270,6 +477,302 @@
}
/**
+ * A conditional operator which yields an integer depending on the boolean operand. This
+ * implements "int result = condition ? value_if_true : value_if_false".
+ *
+ * @since 1.2
+ */
+ static final class ConditionalInt32Op implements DynamicInt32 {
+
+ private final DynamicProto.ConditionalInt32Op mImpl;
+ @Nullable
+ private final Fingerprint mFingerprint;
+
+ ConditionalInt32Op(DynamicProto.ConditionalInt32Op impl, @Nullable Fingerprint fingerprint) {
+ this.mImpl = impl;
+ this.mFingerprint = fingerprint;
+ }
+
+ /**
+ * Gets the condition to use.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public DynamicBool getCondition() {
+ if (mImpl.hasCondition()) {
+ return DynamicBuilders.dynamicBoolFromProto(mImpl.getCondition());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the integer to yield if condition is true.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public DynamicInt32 getValueIfTrue() {
+ if (mImpl.hasValueIfTrue()) {
+ return DynamicBuilders.dynamicInt32FromProto(mImpl.getValueIfTrue());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the integer to yield if condition is false.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public DynamicInt32 getValueIfFalse() {
+ if (mImpl.hasValueIfFalse()) {
+ return DynamicBuilders.dynamicInt32FromProto(mImpl.getValueIfFalse());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public Fingerprint getFingerprint() {
+ return mFingerprint;
+ }
+
+ @NonNull
+ static ConditionalInt32Op fromProto(@NonNull DynamicProto.ConditionalInt32Op proto) {
+ return new ConditionalInt32Op(proto, null);
+ }
+
+ @NonNull
+ DynamicProto.ConditionalInt32Op toProto() {
+ return mImpl;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public DynamicProto.DynamicInt32 toDynamicInt32Proto() {
+ return DynamicProto.DynamicInt32.newBuilder().setConditionalOp(mImpl).build();
+ }
+
+ /**
+ * Builder for {@link ConditionalInt32Op}.
+ */
+ public static final class Builder implements DynamicInt32.Builder {
+
+ private final DynamicProto.ConditionalInt32Op.Builder mImpl =
+ DynamicProto.ConditionalInt32Op.newBuilder();
+ private final Fingerprint mFingerprint = new Fingerprint(1444834226);
+
+ public Builder() {
+ }
+
+ /**
+ * Sets the condition to use.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setCondition(@NonNull DynamicBool condition) {
+ mImpl.setCondition(condition.toDynamicBoolProto());
+ mFingerprint.recordPropertyUpdate(
+ 1, checkNotNull(condition.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ /**
+ * Sets the integer to yield if condition is true.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setValueIfTrue(@NonNull DynamicInt32 valueIfTrue) {
+ mImpl.setValueIfTrue(valueIfTrue.toDynamicInt32Proto());
+ mFingerprint.recordPropertyUpdate(
+ 2, checkNotNull(valueIfTrue.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ /**
+ * Sets the integer to yield if condition is false.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setValueIfFalse(@NonNull DynamicInt32 valueIfFalse) {
+ mImpl.setValueIfFalse(valueIfFalse.toDynamicInt32Proto());
+ mFingerprint.recordPropertyUpdate(
+ 3, checkNotNull(valueIfFalse.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public ConditionalInt32Op build() {
+ return new ConditionalInt32Op(mImpl.build(), mFingerprint);
+ }
+ }
+ }
+
+ /**
+ * A conditional operator which yields a float depending on the boolean operand. This implements
+ * "float result = condition ? value_if_true : value_if_false".
+ *
+ * @since 1.2
+ */
+ static final class ConditionalFloatOp implements DynamicFloat {
+
+ private final DynamicProto.ConditionalFloatOp mImpl;
+ @Nullable
+ private final Fingerprint mFingerprint;
+
+ ConditionalFloatOp(DynamicProto.ConditionalFloatOp impl, @Nullable Fingerprint fingerprint) {
+ this.mImpl = impl;
+ this.mFingerprint = fingerprint;
+ }
+
+ /**
+ * Gets the condition to use.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public DynamicBool getCondition() {
+ if (mImpl.hasCondition()) {
+ return DynamicBuilders.dynamicBoolFromProto(mImpl.getCondition());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the float to yield if condition is true.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public DynamicFloat getValueIfTrue() {
+ if (mImpl.hasValueIfTrue()) {
+ return DynamicBuilders.dynamicFloatFromProto(mImpl.getValueIfTrue());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the float to yield if condition is false.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public DynamicFloat getValueIfFalse() {
+ if (mImpl.hasValueIfFalse()) {
+ return DynamicBuilders.dynamicFloatFromProto(mImpl.getValueIfFalse());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public Fingerprint getFingerprint() {
+ return mFingerprint;
+ }
+
+ @NonNull
+ static ConditionalFloatOp fromProto(@NonNull DynamicProto.ConditionalFloatOp proto) {
+ return new ConditionalFloatOp(proto, null);
+ }
+
+ @NonNull
+ DynamicProto.ConditionalFloatOp toProto() {
+ return mImpl;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public DynamicProto.DynamicFloat toDynamicFloatProto() {
+ return DynamicProto.DynamicFloat.newBuilder().setConditionalOp(mImpl).build();
+ }
+
+ /**
+ * Builder for {@link ConditionalFloatOp}.
+ */
+ public static final class Builder implements DynamicFloat.Builder {
+
+ private final DynamicProto.ConditionalFloatOp.Builder mImpl =
+ DynamicProto.ConditionalFloatOp.newBuilder();
+ private final Fingerprint mFingerprint = new Fingerprint(1968171153);
+
+ public Builder() {
+ }
+
+ /**
+ * Sets the condition to use.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setCondition(@NonNull DynamicBool condition) {
+ mImpl.setCondition(condition.toDynamicBoolProto());
+ mFingerprint.recordPropertyUpdate(
+ 1, checkNotNull(condition.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ /**
+ * Sets the float to yield if condition is true.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setValueIfTrue(@NonNull DynamicFloat valueIfTrue) {
+ mImpl.setValueIfTrue(valueIfTrue.toDynamicFloatProto());
+ mFingerprint.recordPropertyUpdate(
+ 2, checkNotNull(valueIfTrue.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ /**
+ * Sets the float to yield if condition is false.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setValueIfFalse(@NonNull DynamicFloat valueIfFalse) {
+ mImpl.setValueIfFalse(valueIfFalse.toDynamicFloatProto());
+ mFingerprint.recordPropertyUpdate(
+ 3, checkNotNull(valueIfFalse.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public ConditionalFloatOp build() {
+ return new ConditionalFloatOp(mImpl.build(), mFingerprint);
+ }
+ }
+ }
+
+ /**
* Converts a Float to an Int32, with a customizable rounding mode.
*
* @since 1.2
@@ -284,7 +787,7 @@
}
/**
- * Gets the float to round. Intended for testing purposes only.
+ * Gets the float to round.
*
* @since 1.2
*/
@@ -298,8 +801,7 @@
}
/**
- * Gets the rounding mode to use. Defaults to ROUND_MODE_FLOOR if not specified. Intended for
- * testing purposes only.
+ * Gets the rounding mode to use. Defaults to ROUND_MODE_FLOOR if not specified.
*
* @since 1.2
*/
@@ -384,6 +886,26 @@
/**
* Interface defining a dynamic int32 type.
*
+ * <p>It offers a set of helper methods for creating arithmetic and logical expressions, e.g.
+ * {@link #plus(int)}, {@link #times(int)}, {@link #eq(int)}, etc. These helper methods produce
+ * expression trees based on the order in which they were called in an expression. Thus, no
+ * operator precedence rules are applied.
+ *
+ * <p>For example the following expression is equivalent to {@code result = ((a + b)*c)/d }:
+ *
+ * <pre>
+ * a.plus(b).times(c).div(d);
+ * </pre>
+ *
+ * More complex expressions can be created by nesting expressions. For example the following
+ * expression is equivalent to {@code result = (a + b)*(c - d) }:
+ *
+ * <pre>
+ * (a.plus(b)).times(c.minus(d));
+ * </pre>
+ *
+ * .
+ *
* @since 1.2
*/
public interface DynamicInt32 extends DynamicType {
@@ -441,6 +963,670 @@
}
/**
+ * Bind the value of this {@link DynamicInt32} to the result of a conditional expression. This
+ * will use the value given in either {@link ConditionScope#use} or {@link
+ * ConditionScopes.IfTrueScope#elseUse} depending on the value yielded from {@code condition}.
+ */
+ @NonNull
+ static ConditionScope<DynamicInt32, Integer> onCondition(@NonNull DynamicBool condition) {
+ return new ConditionScopes.ConditionScope<>(
+ (trueValue, falseValue) ->
+ new ConditionalInt32Op.Builder()
+ .setCondition(condition)
+ .setValueIfTrue(trueValue)
+ .setValueIfFalse(falseValue)
+ .build(),
+ DynamicInt32::constant);
+ }
+
+ /**
+ * Creates a {@link DynamicInt32} containing the result of adding another {@link DynamicInt32}
+ * to this {@link DynamicInt32}; As an example, the following is equal to {@code
+ * DynamicInt32.constant(13)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).plus(DynamicInt32.constant(6));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicInt32 plus(@NonNull DynamicInt32 other) {
+ return new ArithmeticInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_ADD)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFlaot} containing the result of adding a {@link DynamicFloat} to this
+ * {@link DynamicInt32}; As an example, the following is equal to {@code
+ * DynamicFloat.constant(13.5f)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).plus(DynamicFloat.constant(6.5f));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat plus(@NonNull DynamicFloat other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this.asFloat())
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_ADD)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicInt32} containing the result of adding an integer to this {@link
+ * DynamicInt32}; As an example, the following is equal to {@code DynamicInt32.constant(13)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).plus(6);
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicInt32 plus(int other) {
+ return new ArithmeticInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_ADD)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFlaot} containing the result of adding a float to this {@link
+ * DynamicInt32}; As an example, the following is equal to {@code DynamicFloat.constant(13.5f)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).plus(6.5f);
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat plus(float other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this.asFloat())
+ .setInputRhs(DynamicFloat.constant(other))
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_ADD)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicInt32} containing the result of subtracting another {@link
+ * DynamicInt32} from this {@link DynamicInt32}; As an example, the following is equal to {@code
+ * DynamicInt32.constant(2)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).minus(DynamicInt32.constant(5));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicInt32 minus(@NonNull DynamicInt32 other) {
+ return new ArithmeticInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_SUBTRACT)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of subtracting a {@link DynamicFloat}
+ * from this {@link DynamicInt32}; As an example, the following is equal to {@code
+ * DynamicFloat.constant(1.5f)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).minus(DynamicFloat.constant(5.5f));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat minus(@NonNull DynamicFloat other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this.asFloat())
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_SUBTRACT)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicInt32} containing the result of subtracting an integer from this
+ * {@link DynamicInt32}; As an example, the following is equal to {@code
+ * DynamicInt32.constant(2)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).minus(5);
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicInt32 minus(int other) {
+ return new ArithmeticInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_SUBTRACT)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of subtracting a float from this {@link
+ * DynamicInt32}; As an example, the following is equal to {@code DynamicFloat.constant(1.5f)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).minus(5.5f);
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat minus(float other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this.asFloat())
+ .setInputRhs(DynamicFloat.constant(other))
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_SUBTRACT)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicInt32} containing the result of multiplying this {@link DynamicInt32}
+ * by another {@link DynamicInt32}; As an example, the following is equal to {@code
+ * DynamicInt32.constant(35)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).times(DynamicInt32.constant(5));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicInt32 times(@NonNull DynamicInt32 other) {
+ return new ArithmeticInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MULTIPLY)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of multiplying this {@link DynamicInt32}
+ * by a {@link DynamicFloat}; As an example, the following is equal to {@code
+ * DynamicFloat.constant(38.5f)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).times(DynamicFloat.constant(5.5f));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat times(@NonNull DynamicFloat other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this.asFloat())
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MULTIPLY)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicInt32} containing the result of multiplying this {@link DynamicInt32}
+ * by an integer; As an example, the following is equal to {@code DynamicInt32.constant(35)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).times(5);
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicInt32 times(int other) {
+ return new ArithmeticInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MULTIPLY)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of multiplying this {@link DynamicInt32}
+ * by a float; As an example, the following is equal to {@code DynamicFloat.constant(38.5f)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).times(5.5f);
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat times(float other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this.asFloat())
+ .setInputRhs(DynamicFloat.constant(other))
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MULTIPLY)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicInt32} containing the result of dividing this {@link DynamicInt32} by
+ * another {@link DynamicInt32}; As an example, the following is equal to {@code
+ * DynamicInt32.constant(1)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).div(DynamicInt32.constant(5));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicInt32 div(@NonNull DynamicInt32 other) {
+ return new ArithmeticInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_DIVIDE)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of dividing this {@link DynamicInt32} by
+ * a {@link DynamicFloat}; As an example, the following is equal to {@code
+ * DynamicFloat.constant(1.4f)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).div(DynamicFloat.constant(5f));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat div(@NonNull DynamicFloat other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this.asFloat())
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_DIVIDE)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicInt32} containing the result of dividing this {@link DynamicInt32} by
+ * an integer; As an example, the following is equal to {@code DynamicInt32.constant(1)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).div(5);
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicInt32 div(int other) {
+ return new ArithmeticInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_DIVIDE)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of dividing this {@link DynamicInt32} by
+ * a float; As an example, the following is equal to {@code DynamicFloat.constant(1.4f)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).div(5f);
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat div(float other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this.asFloat())
+ .setInputRhs(DynamicFloat.constant(other))
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_DIVIDE)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicInt32} containing the reminder of dividing this {@link DynamicInt32}
+ * by another {@link DynamicInt32}; As an example, the following is equal to {@code
+ * DynamicInt32.constant(2)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).rem(DynamicInt32.constant(5));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicInt32 rem(@NonNull DynamicInt32 other) {
+ return new ArithmeticInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MODULO)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the reminder of dividing this {@link DynamicInt32}
+ * by a {@link DynamicFloat}; As an example, the following is equal to {@code
+ * DynamicFloat.constant(1.5f)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).rem(DynamicInt32.constant(5.5f));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat rem(@NonNull DynamicFloat other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this.asFloat())
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MODULO)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicInt32} containing the reminder of dividing this {@link DynamicInt32}
+ * by an integer; As an example, the following is equal to {@code DynamicInt32.constant(2)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).rem(5);
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicInt32} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicInt32} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicInt32 rem(int other) {
+ return new ArithmeticInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MODULO)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicInt32} containing the reminder of dividing this {@link DynamicInt32}
+ * by a float; As an example, the following is equal to {@code DynamicFloat.constant(1.5f)}
+ *
+ * <pre>
+ * DynamicInt32.constant(7).rem(5.5f);
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat rem(float other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs( this.asFloat())
+ .setInputRhs(DynamicFloat.constant(other))
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MODULO)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} and
+ * {@code other} are equal, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool eq(@NonNull DynamicInt32 other) {
+ return new ComparisonInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_EQUALS)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} and
+ * {@code other} are equal, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool eq(int other) {
+ return new ComparisonInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_EQUALS)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} and
+ * {@code other} are not equal, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool ne(@NonNull DynamicInt32 other) {
+ return new ComparisonInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_NOT_EQUALS)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} and
+ * {@code other} are not equal, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool ne(int other) {
+ return new ComparisonInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_NOT_EQUALS)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} is less
+ * than {@code other}, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool lt(@NonNull DynamicInt32 other) {
+ return new ComparisonInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_LESS_THAN)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} is less
+ * than {@code other}, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool lt(int other) {
+ return new ComparisonInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_LESS_THAN)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} is less
+ * than or equal to {@code other}, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool lte(@NonNull DynamicInt32 other) {
+ return new ComparisonInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_LESS_THAN_OR_EQUAL_TO)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} is less
+ * than or equal to {@code other}, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool lte(int other) {
+ return new ComparisonInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_LESS_THAN_OR_EQUAL_TO)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} is
+ * greater than {@code other}, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool gt(@NonNull DynamicInt32 other) {
+ return new ComparisonInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_GREATER_THAN)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} is
+ * greater than {@code other}, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool gt(int other) {
+ return new ComparisonInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_GREATER_THAN)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} is
+ * greater than or equal to {@code other}, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool gte(@NonNull DynamicInt32 other) {
+ return new ComparisonInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_GREATER_THAN_OR_EQUAL_TO)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicInt32} is
+ * greater than or equal to {@code other}, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool gte(int other) {
+ return new ComparisonInt32Op.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_GREATER_THAN_OR_EQUAL_TO)
+ .build();
+ }
+
+ /**
* Returns a {@link DynamicString} that contains the formatted value of this {@link
* DynamicInt32} (with default formatting parameters). As an example, in the English locale, the
* following is equal to {@code DynamicString.constant("12")}
@@ -543,9 +1729,15 @@
if (proto.hasFixed()) {
return FixedInt32.fromProto(proto.getFixed());
}
+ if (proto.hasArithmeticOperation()) {
+ return ArithmeticInt32Op.fromProto(proto.getArithmeticOperation());
+ }
if (proto.hasStateSource()) {
return StateInt32Source.fromProto(proto.getStateSource());
}
+ if (proto.hasConditionalOp()) {
+ return ConditionalInt32Op.fromProto(proto.getConditionalOp());
+ }
if (proto.hasFloatToInt()) {
return FloatToInt32Op.fromProto(proto.getFloatToInt());
}
@@ -567,7 +1759,7 @@
}
/**
- * Gets the source of Int32 data to convert to a string. Intended for testing purposes only.
+ * Gets the source of Int32 data to convert to a string.
*
* @since 1.2
*/
@@ -583,8 +1775,7 @@
/**
* Gets minimum integer digits. Sign and grouping characters are not considered when applying
* minIntegerDigits constraint. If not defined, defaults to one. For example,in the English
- * locale, applying minIntegerDigit=4 to 12 would yield "0012". Intended for testing purposes
- * only.
+ * locale, applying minIntegerDigit=4 to 12 would yield "0012".
*
* @since 1.2
*/
@@ -596,7 +1787,7 @@
/**
* Gets digit grouping used. Grouping size and grouping character depend on the current locale.
* If not defined, defaults to false. For example, in the English locale, using grouping with
- * 1234 would yield "1,234". Intended for testing purposes only.
+ * 1234 would yield "1,234".
*
* @since 1.2
*/
@@ -716,7 +1907,7 @@
}
/**
- * Gets the key in the state to bind to. Intended for testing purposes only.
+ * Gets the key in the state to bind to.
*
* @since 1.2
*/
@@ -801,7 +1992,7 @@
}
/**
- * Gets the condition to use. Intended for testing purposes only.
+ * Gets the condition to use.
*
* @since 1.2
*/
@@ -815,7 +2006,7 @@
}
/**
- * Gets the string to yield if condition is true. Intended for testing purposes only.
+ * Gets the string to yield if condition is true.
*
* @since 1.2
*/
@@ -829,7 +2020,7 @@
}
/**
- * Gets the string to yield if condition is false. Intended for testing purposes only.
+ * Gets the string to yield if condition is false.
*
* @since 1.2
*/
@@ -942,6 +2133,7 @@
* @since 1.2
*/
static final class ConcatStringOp implements DynamicString {
+
private final DynamicProto.ConcatStringOp mImpl;
@Nullable private final Fingerprint mFingerprint;
@@ -951,7 +2143,7 @@
}
/**
- * Gets left hand side of the concatenation operation. Intended for testing purposes only.
+ * Gets left hand side of the concatenation operation.
*
* @since 1.2
*/
@@ -965,7 +2157,7 @@
}
/**
- * Gets right hand side of the concatenation operation. Intended for testing purposes only.
+ * Gets right hand side of the concatenation operation.
*
* @since 1.2
*/
@@ -1067,7 +2259,7 @@
}
/**
- * Gets the source of Float data to convert to a string. Intended for testing purposes only.
+ * Gets the source of Float data to convert to a string.
*
* @since 1.2
*/
@@ -1084,7 +2276,7 @@
* Gets maximum fraction digits. Rounding will be applied if maxFractionDigits is smaller than
* number of fraction digits. If not defined, defaults to three. minimumFractionDigits must be
* <= maximumFractionDigits. If the condition is not satisfied, then minimumFractionDigits will
- * be used for both fields. Intended for testing purposes only.
+ * be used for both fields.
*
* @since 1.2
*/
@@ -1097,7 +2289,6 @@
* Gets minimum fraction digits. Zeros will be appended to the end to satisfy this constraint.
* If not defined, defaults to zero. minimumFractionDigits must be <= maximumFractionDigits. If
* the condition is not satisfied, then minimumFractionDigits will be used for both fields.
- * Intended for testing purposes only.
*
* @since 1.2
*/
@@ -1109,8 +2300,7 @@
/**
* Gets minimum integer digits. Sign and grouping characters are not considered when applying
* minIntegerDigits constraint. If not defined, defaults to one. For example, in the English
- * locale, applying minIntegerDigit=4 to 12.34 would yield "0012.34". Intended for testing
- * purposes only.
+ * locale, applying minIntegerDigit=4 to 12.34 would yield "0012.34".
*
* @since 1.2
*/
@@ -1122,7 +2312,7 @@
/**
* Gets digit grouping used. Grouping size and grouping character depend on the current locale.
* If not defined, defaults to false. For example, in the English locale, using grouping with
- * 1234.56 would yield "1,234.56". Intended for testing purposes only.
+ * 1234.56 would yield "1,234.56".
*
* @since 1.2
*/
@@ -1400,6 +2590,150 @@
}
/**
+ * An arithmetic operation, operating on two Float instances. This implements simple binary
+ * operations of the form "result = LHS <op> RHS", where the available operation types are
+ * described in {@code ArithmeticOpType}.
+ *
+ * @since 1.2
+ */
+ static final class ArithmeticFloatOp implements DynamicFloat {
+
+ private final DynamicProto.ArithmeticFloatOp mImpl;
+ @Nullable
+ private final Fingerprint mFingerprint;
+
+ ArithmeticFloatOp(DynamicProto.ArithmeticFloatOp impl, @Nullable Fingerprint fingerprint) {
+ this.mImpl = impl;
+ this.mFingerprint = fingerprint;
+ }
+
+ /**
+ * Gets left hand side of the arithmetic operation.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public DynamicFloat getInputLhs() {
+ if (mImpl.hasInputLhs()) {
+ return DynamicBuilders.dynamicFloatFromProto(mImpl.getInputLhs());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets right hand side of the arithmetic operation.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public DynamicFloat getInputRhs() {
+ if (mImpl.hasInputRhs()) {
+ return DynamicBuilders.dynamicFloatFromProto(mImpl.getInputRhs());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the type of operation to carry out.
+ *
+ * @since 1.2
+ */
+ @ArithmeticOpType
+ public int getOperationType() {
+ return mImpl.getOperationType().getNumber();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public Fingerprint getFingerprint() {
+ return mFingerprint;
+ }
+
+ @NonNull
+ static ArithmeticFloatOp fromProto(@NonNull DynamicProto.ArithmeticFloatOp proto) {
+ return new ArithmeticFloatOp(proto, null);
+ }
+
+ @NonNull
+ DynamicProto.ArithmeticFloatOp toProto() {
+ return mImpl;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public DynamicProto.DynamicFloat toDynamicFloatProto() {
+ return DynamicProto.DynamicFloat.newBuilder().setArithmeticOperation(mImpl).build();
+ }
+
+ /**
+ * Builder for {@link ArithmeticFloatOp}.
+ */
+ public static final class Builder implements DynamicFloat.Builder {
+
+ private final DynamicProto.ArithmeticFloatOp.Builder mImpl =
+ DynamicProto.ArithmeticFloatOp.newBuilder();
+ private final Fingerprint mFingerprint = new Fingerprint(-1818249334);
+
+ public Builder() {
+ }
+
+ /**
+ * Sets left hand side of the arithmetic operation.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setInputLhs(@NonNull DynamicFloat inputLhs) {
+ mImpl.setInputLhs(inputLhs.toDynamicFloatProto());
+ mFingerprint.recordPropertyUpdate(
+ 1, checkNotNull(inputLhs.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ /**
+ * Sets right hand side of the arithmetic operation.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setInputRhs(@NonNull DynamicFloat inputRhs) {
+ mImpl.setInputRhs(inputRhs.toDynamicFloatProto());
+ mFingerprint.recordPropertyUpdate(
+ 2, checkNotNull(inputRhs.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ /**
+ * Sets the type of operation to carry out.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setOperationType(@ArithmeticOpType int operationType) {
+ mImpl.setOperationType(DynamicProto.ArithmeticOpType.forNumber(operationType));
+ mFingerprint.recordPropertyUpdate(3, operationType);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public ArithmeticFloatOp build() {
+ return new ArithmeticFloatOp(mImpl.build(), mFingerprint);
+ }
+ }
+ }
+
+ /**
* A dynamic Float which sources its data from the tile's state.
*
* @since 1.2
@@ -1414,7 +2748,7 @@
}
/**
- * Gets the key in the state to bind to. Intended for testing purposes only.
+ * Gets the key in the state to bind to.
*
* @since 1.2
*/
@@ -1498,7 +2832,7 @@
}
/**
- * Gets the input Int32 to convert to a Float. Intended for testing purposes only.
+ * Gets the input Int32 to convert to a Float.
*
* @since 1.2
*/
@@ -1588,7 +2922,7 @@
}
/**
- * Gets the number to start animating from. Intended for testing purposes only.
+ * Gets the number to start animating from.
*
* @since 1.2
*/
@@ -1597,7 +2931,7 @@
}
/**
- * Gets the number to animate to. Intended for testing purposes only.
+ * Gets the number to animate to.
*
* @since 1.2
*/
@@ -1606,7 +2940,7 @@
}
/**
- * Gets the animation parameters for duration, delay, etc. Intended for testing purposes only.
+ * Gets the animation parameters for duration, delay, etc.
*
* @since 1.2
*/
@@ -1733,7 +3067,7 @@
}
/**
- * Gets the value to watch, and animate when it changes. Intended for testing purposes only.
+ * Gets the value to watch, and animate when it changes.
*
* @since 1.2
*/
@@ -1747,7 +3081,7 @@
}
/**
- * Gets the animation parameters for duration, delay, etc. Intended for testing purposes only.
+ * Gets the animation parameters for duration, delay, etc.
*
* @since 1.2
*/
@@ -1837,6 +3171,26 @@
/**
* Interface defining a dynamic float type.
*
+ * <p>It offers a set of helper methods for creating arithmetic and logical expressions, e.g.
+ * {@link #plus(float)}, {@link #times(float)}, {@link #eq(float)}, etc. These helper methods
+ * produce expression trees based on the order in which they were called in an expression. Thus,
+ * no operator precedence rules are applied.
+ *
+ * <p>For example the following expression is equivalent to {@code result = ((a + b)*c)/d }:
+ *
+ * <pre>
+ * a.plus(b).times(c).div(d);
+ * </pre>
+ *
+ * More complex expressions can be created by nesting expressions. For example the following
+ * expression is equivalent to {@code result = (a + b)*(c - d) }:
+ *
+ * <pre>
+ * (a.plus(b)).times(c.minus(d));
+ * </pre>
+ *
+ * .
+ *
* @since 1.2
*/
public interface DynamicFloat extends DynamicType {
@@ -1979,6 +3333,551 @@
}
/**
+ * Creates a {@link DynamicFloat} containing the result of adding another {@link DynamicFloat}
+ * to this {@link DynamicFloat}; As an example, the following is equal to {@code
+ * DynamicFloat.constant(13f)}
+ *
+ * <pre>
+ * DynamicFloat.constant(7f).plus(DynamicFloat.constant(5f));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat plus(@NonNull DynamicFloat other) {
+
+ // overloaded operators.
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_ADD)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of adding a float to this {@link
+ * DynamicFloat}; As an example, the following is equal to {@code DynamicFloat.constant(13f)}
+ *
+ * <pre>
+ * DynamicFloat.constant(7f).plus(5f);
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat plus(float other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_ADD)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of adding a {@link DynamicInt32} to this
+ * {@link DynamicFloat}; As an example, the following is equal to {@code
+ * DynamicFloat.constant(13f)}
+ *
+ * <pre>
+ * DynamicFloat.constant(7f).plus(DynamicInt32.constant(5));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat plus(@NonNull DynamicInt32 other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other.asFloat())
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_ADD)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of subtracting another {@link
+ * DynamicFloat} from this {@link DynamicFloat}; As an example, the following is equal to {@code
+ * DynamicFloat.constant(2f)}
+ *
+ * <pre>
+ * DynamicFloat.constant(7f).minus(DynamicFloat.constant(5f));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat minus(@NonNull DynamicFloat other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_SUBTRACT)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of subtracting a flaot from this {@link
+ * DynamicFloat}; As an example, the following is equal to {@code DynamicFloat.constant(2f)}
+ *
+ * <pre>
+ * DynamicFloat.constant(7f).minus(5f);
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat minus(float other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_SUBTRACT)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of subtracting a {@link DynamicInt32}
+ * from this {@link DynamicFloat}; As an example, the following is equal to {@code
+ * DynamicFloat.constant(2f)}
+ *
+ * <pre>
+ * DynamicFloat.constant(7f).minus(DynamicInt32.constant(5));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat minus(@NonNull DynamicInt32 other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other.asFloat())
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_SUBTRACT)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of multiplying this {@link DynamicFloat}
+ * by another {@link DynamicFloat}; As an example, the following is equal to {@code
+ * DynamicFloat.constant(35f)}
+ *
+ * <pre>
+ * DynamicFloat.constant(7f).times(DynamicFloat.constant(5f));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat times(@NonNull DynamicFloat other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MULTIPLY)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of multiplying this {@link DynamicFloat}
+ * by a flaot; As an example, the following is equal to {@code DynamicFloat.constant(35f)}
+ *
+ * <pre>
+ * DynamicFloat.constant(7f).times(5f);
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat times(float other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MULTIPLY)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of multiplying this {@link DynamicFloat}
+ * by a {@link DynamicInt32}; As an example, the following is equal to {@code
+ * DynamicFloat.constant(35f)}
+ *
+ * <pre>
+ * DynamicFloat.constant(7f).times(DynamicInt32.constant(5));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat times(@NonNull DynamicInt32 other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other.asFloat())
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MULTIPLY)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of dividing this {@link DynamicFloat} by
+ * another {@link DynamicFloat}; As an example, the following is equal to {@code
+ * DynamicFloat.constant(1.4f)}
+ *
+ * <pre>
+ * DynamicFloat.constant(7f).div(DynamicFloat.constant(5f));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat div(@NonNull DynamicFloat other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_DIVIDE)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of dividing this {@link DynamicFloat} by
+ * a float; As an example, the following is equal to {@code DynamicFloat.constant(1.4f)}
+ *
+ * <pre>
+ * DynamicFloat.constant(7f).div(5f);
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat div(float other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_DIVIDE)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the result of dividing this {@link DynamicFloat} by
+ * a {@link DynamicInt32}; As an example, the following is equal to {@code
+ * DynamicFloat.constant(1.4f)}
+ *
+ * <pre>
+ * DynamicFloat.constant(7f).div(DynamicInt32.constant(5));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat div(@NonNull DynamicInt32 other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other.asFloat())
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_DIVIDE)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicFloat} containing the reminder of dividing this {@link DynamicFloat}
+ * by another {@link DynamicFloat}; As an example, the following is equal to {@code
+ * DynamicFloat.constant(1.5f)}
+ *
+ * <pre>
+ * DynamicFloat.constant(7f).rem(DynamicFloat.constant(5.5f));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat rem(@NonNull DynamicFloat other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MODULO)
+ .build();
+ }
+
+ /**
+ * reates a {@link DynamicFloat} containing the reminder of dividing this {@link DynamicFloat}
+ * by a float; As an example, the following is equal to {@code DynamicFloat.constant(1.5f)}
+ *
+ * <pre>
+ * DynamicFloat.constant(7f).rem(5.5f);
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat rem(float other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MODULO)
+ .build();
+ }
+
+ /**
+ * reates a {@link DynamicFloat} containing the reminder of dividing this {@link DynamicFloat}
+ * by a {@link DynamicInt32}; As an example, the following is equal to {@code
+ * DynamicFloat.constant(2f)}
+ *
+ * <pre>
+ * DynamicFloat.constant(7f).rem(DynamicInt32.constant(5));
+ * </pre>
+ *
+ * The operation's evaluation order depends only on its position in the expression; no operator
+ * precedence rules are applied. See {@link DynamicFloat} for more information on operation
+ * evaluation order.
+ *
+ * @return a new instance of {@link DynamicFloat} containing the result of the operation.
+ */
+ @SuppressWarnings("KotlinOperator")
+ @NonNull
+ default DynamicFloat rem(@NonNull DynamicInt32 other) {
+ return new ArithmeticFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other.asFloat())
+ .setOperationType(DynamicBuilders.ARITHMETIC_OP_TYPE_MODULO)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} and
+ * {@code other} are equal, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool eq(@NonNull DynamicFloat other) {
+ return new ComparisonFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_EQUALS)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} and
+ * {@code other} are equal, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool eq(float other) {
+ return new ComparisonFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_EQUALS)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} and
+ * {@code other} are not equal, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool ne(@NonNull DynamicFloat other) {
+ return new ComparisonFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_NOT_EQUALS)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} and
+ * {@code other} are not equal, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool ne(float other) {
+ return new ComparisonFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_NOT_EQUALS)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} is less
+ * than {@code other}, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool lt(@NonNull DynamicFloat other) {
+ return new ComparisonFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_LESS_THAN)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} is less
+ * than {@code other}, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool lt(float other) {
+ return new ComparisonFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_LESS_THAN)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} is less
+ * than or equal to {@code other}, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool lte(@NonNull DynamicFloat other) {
+ return new ComparisonFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_LESS_THAN_OR_EQUAL_TO)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} is less
+ * than or equal to {@code other}, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool lte(float other) {
+ return new ComparisonFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_LESS_THAN_OR_EQUAL_TO)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} is
+ * greater than {@code other}, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool gt(@NonNull DynamicFloat other) {
+ return new ComparisonFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_GREATER_THAN)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} is
+ * greater than {@code other}, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool gt(float other) {
+ return new ComparisonFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_GREATER_THAN)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} is
+ * greater than or equal to {@code other}, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool gte(@NonNull DynamicFloat other) {
+ return new ComparisonFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(other)
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_GREATER_THAN_OR_EQUAL_TO)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicBool} that is true if the value of this {@link DynamicFloat} is
+ * greater than or equal to {@code other}, otherwise it's false.
+ */
+ @NonNull
+ default DynamicBool gte(float other) {
+ return new ComparisonFloatOp.Builder()
+ .setInputLhs(this)
+ .setInputRhs(constant(other))
+ .setOperationType(DynamicBuilders.COMPARISON_OP_TYPE_GREATER_THAN_OR_EQUAL_TO)
+ .build();
+ }
+
+ /**
+ * Bind the value of this {@link DynamicFloat} to the result of a conditional expression. This
+ * will use the value given in either {@link ConditionScope#use} or {@link
+ * ConditionScopes.IfTrueScope#elseUse} depending on the value yielded from {@code condition}.
+ */
+ @NonNull
+ static ConditionScope<DynamicFloat, Float> onCondition(@NonNull DynamicBool condition) {
+ return new ConditionScopes.ConditionScope<>(
+ (trueValue, falseValue) ->
+ new ConditionalFloatOp.Builder()
+ .setCondition(condition)
+ .setValueIfTrue(trueValue)
+ .setValueIfFalse(falseValue)
+ .build(),
+ DynamicFloat::constant);
+ }
+
+ /**
* Returns a {@link DynamicString} that contains the formatted value of this {@link
* DynamicFloat} (with default formatting parameters). As an example, in the English locale, the
* following is equal to {@code DynamicString.constant("12.346")}
@@ -2103,12 +4002,18 @@
if (proto.hasFixed()) {
return FixedFloat.fromProto(proto.getFixed());
}
+ if (proto.hasArithmeticOperation()) {
+ return ArithmeticFloatOp.fromProto(proto.getArithmeticOperation());
+ }
if (proto.hasInt32ToFloatOperation()) {
return Int32ToFloatOp.fromProto(proto.getInt32ToFloatOperation());
}
if (proto.hasStateSource()) {
return StateFloatSource.fromProto(proto.getStateSource());
}
+ if (proto.hasConditionalOp()) {
+ return ConditionalFloatOp.fromProto(proto.getConditionalOp());
+ }
if (proto.hasAnimatableFixed()) {
return AnimatableFixedFloat.fromProto(proto.getAnimatableFixed());
}
@@ -2133,7 +4038,7 @@
}
/**
- * Gets the key in the state to bind to. Intended for testing purposes only.
+ * Gets the key in the state to bind to.
*
* @since 1.2
*/
@@ -2176,6 +4081,7 @@
/** Builder for {@link StateBoolSource}. */
public static final class Builder implements DynamicBool.Builder {
+
private final DynamicProto.StateBoolSource.Builder mImpl =
DynamicProto.StateBoolSource.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(1818702779);
@@ -2203,6 +4109,294 @@
}
/**
+ * A comparison operation, operating on two Int32 instances. This implements various comparison
+ * operations of the form "boolean result = LHS <op> RHS", where the available operation types are
+ * described in {@code ComparisonOpType}.
+ *
+ * @since 1.2
+ */
+ static final class ComparisonInt32Op implements DynamicBool {
+
+ private final DynamicProto.ComparisonInt32Op mImpl;
+ @Nullable
+ private final Fingerprint mFingerprint;
+
+ ComparisonInt32Op(DynamicProto.ComparisonInt32Op impl, @Nullable Fingerprint fingerprint) {
+ this.mImpl = impl;
+ this.mFingerprint = fingerprint;
+ }
+
+ /**
+ * Gets the left hand side of the comparison operation.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public DynamicInt32 getInputLhs() {
+ if (mImpl.hasInputLhs()) {
+ return DynamicBuilders.dynamicInt32FromProto(mImpl.getInputLhs());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the right hand side of the comparison operation.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public DynamicInt32 getInputRhs() {
+ if (mImpl.hasInputRhs()) {
+ return DynamicBuilders.dynamicInt32FromProto(mImpl.getInputRhs());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the type of the operation.
+ *
+ * @since 1.2
+ */
+ @ComparisonOpType
+ public int getOperationType() {
+ return mImpl.getOperationType().getNumber();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public Fingerprint getFingerprint() {
+ return mFingerprint;
+ }
+
+ @NonNull
+ static ComparisonInt32Op fromProto(@NonNull DynamicProto.ComparisonInt32Op proto) {
+ return new ComparisonInt32Op(proto, null);
+ }
+
+ @NonNull
+ DynamicProto.ComparisonInt32Op toProto() {
+ return mImpl;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public DynamicProto.DynamicBool toDynamicBoolProto() {
+ return DynamicProto.DynamicBool.newBuilder().setInt32Comparison(mImpl).build();
+ }
+
+ /**
+ * Builder for {@link ComparisonInt32Op}.
+ */
+ public static final class Builder implements DynamicBool.Builder {
+
+ private final DynamicProto.ComparisonInt32Op.Builder mImpl =
+ DynamicProto.ComparisonInt32Op.newBuilder();
+ private final Fingerprint mFingerprint = new Fingerprint(-1112207999);
+
+ public Builder() {
+ }
+
+ /**
+ * Sets the left hand side of the comparison operation.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setInputLhs(@NonNull DynamicInt32 inputLhs) {
+ mImpl.setInputLhs(inputLhs.toDynamicInt32Proto());
+ mFingerprint.recordPropertyUpdate(
+ 1, checkNotNull(inputLhs.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ /**
+ * Sets the right hand side of the comparison operation.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setInputRhs(@NonNull DynamicInt32 inputRhs) {
+ mImpl.setInputRhs(inputRhs.toDynamicInt32Proto());
+ mFingerprint.recordPropertyUpdate(
+ 2, checkNotNull(inputRhs.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ /**
+ * Sets the type of the operation.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setOperationType(@ComparisonOpType int operationType) {
+ mImpl.setOperationType(DynamicProto.ComparisonOpType.forNumber(operationType));
+ mFingerprint.recordPropertyUpdate(3, operationType);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public ComparisonInt32Op build() {
+ return new ComparisonInt32Op(mImpl.build(), mFingerprint);
+ }
+ }
+ }
+
+ /**
+ * A comparison operation, operating on two Float instances. This implements various comparison
+ * operations of the form "boolean result = LHS <op> RHS", where the available operation types are
+ * described in {@code ComparisonOpType}.
+ *
+ * @since 1.2
+ */
+ static final class ComparisonFloatOp implements DynamicBool {
+
+ private final DynamicProto.ComparisonFloatOp mImpl;
+ @Nullable
+ private final Fingerprint mFingerprint;
+
+ ComparisonFloatOp(DynamicProto.ComparisonFloatOp impl, @Nullable Fingerprint fingerprint) {
+ this.mImpl = impl;
+ this.mFingerprint = fingerprint;
+ }
+
+ /**
+ * Gets the left hand side of the comparison operation.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public DynamicFloat getInputLhs() {
+ if (mImpl.hasInputLhs()) {
+ return DynamicBuilders.dynamicFloatFromProto(mImpl.getInputLhs());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the right hand side of the comparison operation.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public DynamicFloat getInputRhs() {
+ if (mImpl.hasInputRhs()) {
+ return DynamicBuilders.dynamicFloatFromProto(mImpl.getInputRhs());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the type of the operation.
+ *
+ * @since 1.2
+ */
+ @ComparisonOpType
+ public int getOperationType() {
+ return mImpl.getOperationType().getNumber();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public Fingerprint getFingerprint() {
+ return mFingerprint;
+ }
+
+ @NonNull
+ static ComparisonFloatOp fromProto(@NonNull DynamicProto.ComparisonFloatOp proto) {
+ return new ComparisonFloatOp(proto, null);
+ }
+
+ @NonNull
+ DynamicProto.ComparisonFloatOp toProto() {
+ return mImpl;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public DynamicProto.DynamicBool toDynamicBoolProto() {
+ return DynamicProto.DynamicBool.newBuilder().setFloatComparison(mImpl).build();
+ }
+
+ /**
+ * Builder for {@link ComparisonFloatOp}.
+ */
+ public static final class Builder implements DynamicBool.Builder {
+
+ private final DynamicProto.ComparisonFloatOp.Builder mImpl =
+ DynamicProto.ComparisonFloatOp.newBuilder();
+ private final Fingerprint mFingerprint = new Fingerprint(-1679565270);
+
+ public Builder() {
+ }
+
+ /**
+ * Sets the left hand side of the comparison operation.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setInputLhs(@NonNull DynamicFloat inputLhs) {
+ mImpl.setInputLhs(inputLhs.toDynamicFloatProto());
+ mFingerprint.recordPropertyUpdate(
+ 1, checkNotNull(inputLhs.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ /**
+ * Sets the right hand side of the comparison operation.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setInputRhs(@NonNull DynamicFloat inputRhs) {
+ mImpl.setInputRhs(inputRhs.toDynamicFloatProto());
+ mFingerprint.recordPropertyUpdate(
+ 2, checkNotNull(inputRhs.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ /**
+ * Sets the type of the operation.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setOperationType(@ComparisonOpType int operationType) {
+ mImpl.setOperationType(DynamicProto.ComparisonOpType.forNumber(operationType));
+ mFingerprint.recordPropertyUpdate(3, operationType);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public ComparisonFloatOp build() {
+ return new ComparisonFloatOp(mImpl.build(), mFingerprint);
+ }
+ }
+ }
+
+ /**
* A boolean operation which implements a "NOT" operator, i.e. "boolean result = !input".
*
* @since 1.2
@@ -2217,7 +4411,7 @@
}
/**
- * Gets the input, whose value to negate. Intended for testing purposes only.
+ * Gets the input, whose value to negate.
*
* @since 1.2
*/
@@ -2306,7 +4500,7 @@
}
/**
- * Gets the left hand side of the logical operation. Intended for testing purposes only.
+ * Gets the left hand side of the logical operation.
*
* @since 1.2
*/
@@ -2320,7 +4514,7 @@
}
/**
- * Gets the right hand side of the logical operation. Intended for testing purposes only.
+ * Gets the right hand side of the logical operation.
*
* @since 1.2
*/
@@ -2334,7 +4528,7 @@
}
/**
- * Gets the operation type to apply to LHS/RHS. Intended for testing purposes only.
+ * Gets the operation type to apply to LHS/RHS.
*
* @since 1.2
*/
@@ -2572,12 +4766,18 @@
if (proto.hasStateSource()) {
return StateBoolSource.fromProto(proto.getStateSource());
}
+ if (proto.hasInt32Comparison()) {
+ return ComparisonInt32Op.fromProto(proto.getInt32Comparison());
+ }
if (proto.hasNotOp()) {
return NotBoolOp.fromProto(proto.getNotOp());
}
if (proto.hasLogicalOp()) {
return LogicalBoolOp.fromProto(proto.getLogicalOp());
}
+ if (proto.hasFloatComparison()) {
+ return ComparisonFloatOp.fromProto(proto.getFloatComparison());
+ }
throw new IllegalStateException("Proto was not a recognised instance of DynamicBool");
}
@@ -2596,7 +4796,7 @@
}
/**
- * Gets the key in the state to bind to. Intended for testing purposes only.
+ * Gets the key in the state to bind to.
*
* @since 1.2
*/
@@ -2681,8 +4881,7 @@
}
/**
- * Gets the color value (in ARGB format) to start animating from. Intended for testing purposes
- * only.
+ * Gets the color value (in ARGB format) to start animating from.
*
* @since 1.2
*/
@@ -2692,7 +4891,7 @@
}
/**
- * Gets the color value (in ARGB format) to animate to. Intended for testing purposes only.
+ * Gets the color value (in ARGB format) to animate to.
*
* @since 1.2
*/
@@ -2702,7 +4901,7 @@
}
/**
- * Gets the animation parameters for duration, delay, etc. Intended for testing purposes only.
+ * Gets the animation parameters for duration, delay, etc.
*
* @since 1.2
*/
@@ -2829,7 +5028,7 @@
}
/**
- * Gets the value to watch, and animate when it changes. Intended for testing purposes only.
+ * Gets the value to watch, and animate when it changes.
*
* @since 1.2
*/
@@ -2843,7 +5042,7 @@
}
/**
- * Gets the animation parameters for duration, delay, etc. Intended for testing purposes only.
+ * Gets the animation parameters for duration, delay, etc.
*
* @since 1.2
*/
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/animation_parameters.proto b/wear/protolayout/protolayout-proto/src/main/proto/animation_parameters.proto
index 094a273..aee13fe 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/animation_parameters.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/animation_parameters.proto
@@ -16,7 +16,7 @@
// 0.
uint32 delay_millis = 2;
- // The easing to be used for adjusting an animation’s fraction. If not set,
+ // The easing to be used for adjusting an animation's fraction. If not set,
// defaults to Linear Interpolator.
Easing easing = 3;
@@ -25,17 +25,17 @@
Repeatable repeatable = 5;
}
-// The easing to be used for adjusting an animation’s fraction. This allows
+// The easing to be used for adjusting an animation's fraction. This allows
// animation to speed up and slow down, rather than moving at a constant rate.
// If not set, defaults to Linear Interpolator.
message Easing {
oneof inner {
- // The cubic polynomial easing that implements third-order Bézier curves.
+ // The cubic polynomial easing that implements third-order Bezier curves.
CubicBezierEasing cubic_bezier = 1;
}
}
-// The cubic polynomial easing that implements third-order Bézier curves. This
+// The cubic polynomial easing that implements third-order Bezier curves. This
// is equivalent to the Android PathInterpolator.
message CubicBezierEasing {
// The x coordinate of the first control point. The line through the point (0,
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/DeviceConfig.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/DeviceConfig.kt
index 220def1..bc924c6 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/DeviceConfig.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/DeviceConfig.kt
@@ -16,10 +16,9 @@
package androidx.wear.watchface.client
+import androidx.wear.watchface.data.DeviceConfig as WireDeviceConfig
import androidx.annotation.RestrictTo
-typealias WireDeviceConfig = androidx.wear.watchface.data.DeviceConfig
-
/**
* Describes the hardware configuration of the device the watch face is running on.
*
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
index 91bc15b..2bc3ba1 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
@@ -502,6 +502,7 @@
it.value.asWireComplicationData()
)
},
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
diff --git a/wear/watchface/watchface-complications-data-source/api/current.ignore b/wear/watchface/watchface-complications-data-source/api/current.ignore
new file mode 100644
index 0000000..bc01d17
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-source/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: androidx.wear.watchface.complications.datasource.ComplicationDataTimelineKt:
+ Removed class androidx.wear.watchface.complications.datasource.ComplicationDataTimelineKt
diff --git a/wear/watchface/watchface-complications-data-source/api/current.txt b/wear/watchface/watchface-complications-data-source/api/current.txt
index 0aaf430..73e5e61 100644
--- a/wear/watchface/watchface-complications-data-source/api/current.txt
+++ b/wear/watchface/watchface-complications-data-source/api/current.txt
@@ -51,9 +51,6 @@
property public final java.util.Collection<androidx.wear.watchface.complications.datasource.TimelineEntry> timelineEntries;
}
- public final class ComplicationDataTimelineKt {
- }
-
public final class ComplicationRequest {
ctor public ComplicationRequest(int complicationInstanceId, androidx.wear.watchface.complications.data.ComplicationType complicationType, boolean immediateResponseRequired);
ctor @Deprecated public ComplicationRequest(int complicationInstanceId, androidx.wear.watchface.complications.data.ComplicationType complicationType);
diff --git a/wear/watchface/watchface-complications-data-source/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data-source/api/public_plus_experimental_current.txt
index 0aaf430..73e5e61 100644
--- a/wear/watchface/watchface-complications-data-source/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-complications-data-source/api/public_plus_experimental_current.txt
@@ -51,9 +51,6 @@
property public final java.util.Collection<androidx.wear.watchface.complications.datasource.TimelineEntry> timelineEntries;
}
- public final class ComplicationDataTimelineKt {
- }
-
public final class ComplicationRequest {
ctor public ComplicationRequest(int complicationInstanceId, androidx.wear.watchface.complications.data.ComplicationType complicationType, boolean immediateResponseRequired);
ctor @Deprecated public ComplicationRequest(int complicationInstanceId, androidx.wear.watchface.complications.data.ComplicationType complicationType);
diff --git a/wear/watchface/watchface-complications-data-source/api/restricted_current.ignore b/wear/watchface/watchface-complications-data-source/api/restricted_current.ignore
new file mode 100644
index 0000000..bc01d17
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-source/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: androidx.wear.watchface.complications.datasource.ComplicationDataTimelineKt:
+ Removed class androidx.wear.watchface.complications.datasource.ComplicationDataTimelineKt
diff --git a/wear/watchface/watchface-complications-data-source/api/restricted_current.txt b/wear/watchface/watchface-complications-data-source/api/restricted_current.txt
index 0aaf430..73e5e61 100644
--- a/wear/watchface/watchface-complications-data-source/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications-data-source/api/restricted_current.txt
@@ -51,9 +51,6 @@
property public final java.util.Collection<androidx.wear.watchface.complications.datasource.TimelineEntry> timelineEntries;
}
- public final class ComplicationDataTimelineKt {
- }
-
public final class ComplicationRequest {
ctor public ComplicationRequest(int complicationInstanceId, androidx.wear.watchface.complications.data.ComplicationType complicationType, boolean immediateResponseRequired);
ctor @Deprecated public ComplicationRequest(int complicationInstanceId, androidx.wear.watchface.complications.data.ComplicationType complicationType);
diff --git a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
index 868b87e..68018b6 100644
--- a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
+++ b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
@@ -16,13 +16,11 @@
package androidx.wear.watchface.complications.datasource
+import android.support.wearable.complications.ComplicationData as WireComplicationData
import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.data.NoDataComplicationData
import java.time.Instant
-/** The wire format for [ComplicationData]. */
-internal typealias WireComplicationData = android.support.wearable.complications.ComplicationData
-
/**
* A time interval, typically used to describe the validity period of a [TimelineEntry].
*
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
index 66f4757..afa5db9 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
@@ -16,6 +16,7 @@
package androidx.wear.watchface.complications.data
+import android.support.wearable.complications.ComplicationData as WireComplicationData
import androidx.annotation.GuardedBy
import androidx.annotation.RestrictTo
import java.util.concurrent.Executor
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index a8ef73db..21f4976 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -18,6 +18,8 @@
package androidx.wear.watchface.complications.data
+import android.support.wearable.complications.ComplicationData as WireComplicationData
+import android.support.wearable.complications.ComplicationData.Builder as WireComplicationDataBuilder
import android.app.PendingIntent
import android.content.ComponentName
import android.graphics.Color
@@ -37,13 +39,6 @@
import androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element
import java.time.Instant
-/** The wire format for [ComplicationData]. */
-internal typealias WireComplicationData = android.support.wearable.complications.ComplicationData
-
-/** The builder for [WireComplicationData]. */
-internal typealias WireComplicationDataBuilder =
- android.support.wearable.complications.ComplicationData.Builder
-
internal const val TAG = "Data.kt"
/**
@@ -141,18 +136,23 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public abstract fun asWireComplicationData(): WireComplicationData
+ public fun asWireComplicationData(): WireComplicationData {
+ cachedWireComplicationData?.let { return it }
+ return createWireComplicationDataBuilder()
+ .apply { fillWireComplicationDataBuilder(this) }
+ .build()
+ .also { cachedWireComplicationData = it }
+ }
internal fun createWireComplicationDataBuilder(): WireComplicationDataBuilder =
cachedWireComplicationData?.let {
WireComplicationDataBuilder(it)
- } ?: WireComplicationDataBuilder(type.toWireComplicationType()).apply {
- setDataSource(dataSource)
- setPersistencePolicy(persistencePolicy)
- setDisplayPolicy(displayPolicy)
- }
+ } ?: WireComplicationDataBuilder(type.toWireComplicationType())
internal open fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+ builder.setDataSource(dataSource)
+ builder.setPersistencePolicy(persistencePolicy)
+ builder.setDisplayPolicy(displayPolicy)
}
/**
@@ -196,6 +196,14 @@
internal var persistencePolicy = ComplicationPersistencePolicies.CACHING_ALLOWED
internal var displayPolicy = ComplicationDisplayPolicies.ALWAYS_DISPLAY
+ @Suppress("NewApi")
+ internal fun setCommon(data: WireComplicationData) = apply {
+ setCachedWireComplicationData(data)
+ setDataSource(data.dataSource)
+ setPersistencePolicy(data.persistencePolicy)
+ setDisplayPolicy(data.displayPolicy)
+ }
+
/**
* Sets the [ComponentName] of the ComplicationDataSourceService that provided this
* ComplicationData, if any.
@@ -299,21 +307,15 @@
else -> null
}
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- override fun asWireComplicationData(): WireComplicationData {
- cachedWireComplicationData?.let {
- return it
+ override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+ super.fillWireComplicationDataBuilder(builder)
+ if (placeholder == null) {
+ builder.setPlaceholder(null)
+ } else {
+ val placeholderBuilder = placeholder.createWireComplicationDataBuilder()
+ placeholder.fillWireComplicationDataBuilder(placeholderBuilder)
+ builder.setPlaceholder(placeholderBuilder.build())
}
- return createWireComplicationDataBuilder().apply {
- if (placeholder == null) {
- setPlaceholder(null)
- } else {
- val builder = placeholder.createWireComplicationDataBuilder()
- placeholder.fillWireComplicationDataBuilder(builder)
- setPlaceholder(builder.build())
- }
- }.build().also { cachedWireComplicationData = it }
}
override fun toString(): String {
@@ -345,9 +347,8 @@
persistencePolicy = ComplicationPersistencePolicies.CACHING_ALLOWED,
displayPolicy = ComplicationDisplayPolicies.ALWAYS_DISPLAY
) {
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- override fun asWireComplicationData(): WireComplicationData = asPlainWireComplicationData(type)
+ // Always empty.
+ override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {}
override fun toString(): String {
return "EmptyComplicationData()"
@@ -375,9 +376,8 @@
persistencePolicy = ComplicationPersistencePolicies.CACHING_ALLOWED,
displayPolicy = ComplicationDisplayPolicies.ALWAYS_DISPLAY
) {
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- override fun asWireComplicationData(): WireComplicationData = asPlainWireComplicationData(type)
+ // Always empty.
+ override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {}
override fun toString(): String {
return "NotConfiguredComplicationData()"
@@ -515,18 +515,8 @@
)
}
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- override fun asWireComplicationData(): WireComplicationData {
- cachedWireComplicationData?.let {
- return it
- }
- return createWireComplicationDataBuilder().apply {
- fillWireComplicationDataBuilder(this)
- }.build().also { cachedWireComplicationData = it }
- }
-
override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+ super.fillWireComplicationDataBuilder(builder)
builder.setShortText(text.toWireComplicationText())
builder.setShortTitle(title?.toWireComplicationText())
builder.setContentDescription(
@@ -696,18 +686,8 @@
)
}
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- override fun asWireComplicationData(): WireComplicationData {
- cachedWireComplicationData?.let {
- return it
- }
- return createWireComplicationDataBuilder().apply {
- fillWireComplicationDataBuilder(this)
- }.build().also { cachedWireComplicationData = it }
- }
-
override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+ super.fillWireComplicationDataBuilder(builder)
builder.setLongText(text.toWireComplicationText())
builder.setLongTitle(title?.toWireComplicationText())
monochromaticImage?.addToWireComplicationData(builder)
@@ -1062,18 +1042,8 @@
}
}
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public override fun asWireComplicationData(): WireComplicationData {
- cachedWireComplicationData?.let {
- return it
- }
- return createWireComplicationDataBuilder().apply {
- fillWireComplicationDataBuilder(this)
- }.build().also { cachedWireComplicationData = it }
- }
-
override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+ super.fillWireComplicationDataBuilder(builder)
builder.setRangedValue(value)
builder.setRangedValueExpression(valueExpression)
builder.setRangedMinValue(min)
@@ -1397,18 +1367,8 @@
}
}
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public override fun asWireComplicationData(): WireComplicationData {
- cachedWireComplicationData?.let {
- return it
- }
- return createWireComplicationDataBuilder().apply {
- fillWireComplicationDataBuilder(this)
- }.build().also { cachedWireComplicationData = it }
- }
-
override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+ super.fillWireComplicationDataBuilder(builder)
builder.setRangedValue(value)
builder.setRangedValueExpression(valueExpression)
builder.setTargetValue(targetValue)
@@ -1714,18 +1674,8 @@
}
}
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public override fun asWireComplicationData(): WireComplicationData {
- cachedWireComplicationData?.let {
- return it
- }
- return createWireComplicationDataBuilder().apply {
- fillWireComplicationDataBuilder(this)
- }.build().also { cachedWireComplicationData = it }
- }
-
override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+ super.fillWireComplicationDataBuilder(builder)
builder.setElementWeights(elements.map { it.weight }.toFloatArray())
builder.setElementColors(elements.map { it.color }.toIntArray())
builder.setElementBackgroundColor(elementBackgroundColor)
@@ -1872,18 +1822,8 @@
)
}
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- override fun asWireComplicationData(): WireComplicationData {
- cachedWireComplicationData?.let {
- return it
- }
- return createWireComplicationDataBuilder().apply {
- fillWireComplicationDataBuilder(this)
- }.build().also { cachedWireComplicationData = it }
- }
-
override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+ super.fillWireComplicationDataBuilder(builder)
monochromaticImage.addToWireComplicationData(builder)
builder.setContentDescription(
when (contentDescription) {
@@ -1988,18 +1928,8 @@
)
}
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- override fun asWireComplicationData(): WireComplicationData {
- cachedWireComplicationData?.let {
- return it
- }
- return createWireComplicationDataBuilder().apply {
- fillWireComplicationDataBuilder(this)
- }.build().also { cachedWireComplicationData = it }
- }
-
override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+ super.fillWireComplicationDataBuilder(builder)
smallImage.addToWireComplicationData(builder)
builder.setContentDescription(
when (contentDescription) {
@@ -2110,18 +2040,8 @@
)
}
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- override fun asWireComplicationData(): WireComplicationData {
- cachedWireComplicationData?.let {
- return it
- }
- return createWireComplicationDataBuilder().apply {
- fillWireComplicationDataBuilder(this)
- }.build().also { cachedWireComplicationData = it }
- }
-
override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+ super.fillWireComplicationDataBuilder(builder)
builder.setLargeImage(photoImage)
builder.setContentDescription(
when (contentDescription) {
@@ -2250,18 +2170,12 @@
)
}
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- override fun asWireComplicationData(): WireComplicationData {
- cachedWireComplicationData?.let {
- return it
- }
- return createWireComplicationDataBuilder().apply {
- setShortText(text?.toWireComplicationText())
- setShortTitle(title?.toWireComplicationText())
- monochromaticImage?.addToWireComplicationData(this)
- smallImage?.addToWireComplicationData(this)
- }.build().also { cachedWireComplicationData = it }
+ override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
+ super.fillWireComplicationDataBuilder(builder)
+ builder.setShortText(text?.toWireComplicationText())
+ builder.setShortTitle(title?.toWireComplicationText())
+ monochromaticImage?.addToWireComplicationData(builder)
+ smallImage?.addToWireComplicationData(builder)
}
override fun toString(): String {
@@ -2460,16 +2374,11 @@
@Suppress("NewApi")
public fun WireComplicationData.toApiComplicationData(): ComplicationData {
try {
- // Make sure we use the correct dataSource, persistencePolicy & displayPolicy.
- val dataSourceCopy = dataSource
- val persistencePolicyCopy = persistencePolicy
- val displayPolicyCopy = displayPolicy
- val wireComplicationData = this
return when (type) {
NoDataComplicationData.TYPE.toWireComplicationType() -> {
placeholder?.toPlaceholderComplicationData()?.let {
- NoDataComplicationData(it)
- } ?: NoDataComplicationData()
+ NoDataComplicationData(it, this@toApiComplicationData)
+ } ?: NoDataComplicationData(null, this@toApiComplicationData)
}
EmptyComplicationData.TYPE.toWireComplicationType() -> EmptyComplicationData()
@@ -2482,15 +2391,12 @@
shortText!!.toApiComplicationText(),
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
).apply {
+ setCommon(this@toApiComplicationData)
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
setTitle(shortTitle?.toApiComplicationText())
setMonochromaticImage(parseIcon())
setSmallImage(parseSmallImage())
- setCachedWireComplicationData(wireComplicationData)
- setDataSource(dataSourceCopy)
- setPersistencePolicy(persistencePolicyCopy)
- setDisplayPolicy(displayPolicyCopy)
}.build()
LongTextComplicationData.TYPE.toWireComplicationType() ->
@@ -2498,15 +2404,12 @@
longText!!.toApiComplicationText(),
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
).apply {
+ setCommon(this@toApiComplicationData)
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
setTitle(longTitle?.toApiComplicationText())
setMonochromaticImage(parseIcon())
setSmallImage(parseSmallImage())
- setCachedWireComplicationData(wireComplicationData)
- setDataSource(dataSourceCopy)
- setPersistencePolicy(persistencePolicyCopy)
- setDisplayPolicy(displayPolicyCopy)
}.build()
RangedValueComplicationData.TYPE.toWireComplicationType() ->
@@ -2518,19 +2421,16 @@
contentDescription = contentDescription?.toApiComplicationText()
?: ComplicationText.EMPTY
).apply {
+ setCommon(this@toApiComplicationData)
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
setMonochromaticImage(parseIcon())
setSmallImage(parseSmallImage())
setTitle(shortTitle?.toApiComplicationText())
setText(shortText?.toApiComplicationText())
- setCachedWireComplicationData(wireComplicationData)
colorRamp?.let {
setColorRamp(ColorRamp(it, isColorRampInterpolated!!))
}
- setDataSource(dataSourceCopy)
- setPersistencePolicy(persistencePolicyCopy)
- setDisplayPolicy(displayPolicyCopy)
setValueType(rangedValueType)
}.build()
@@ -2539,12 +2439,9 @@
parseIcon()!!,
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
).apply {
+ setCommon(this@toApiComplicationData)
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
- setCachedWireComplicationData(wireComplicationData)
- setDataSource(dataSourceCopy)
- setPersistencePolicy(persistencePolicyCopy)
- setDisplayPolicy(displayPolicyCopy)
}.build()
SmallImageComplicationData.TYPE.toWireComplicationType() ->
@@ -2552,12 +2449,9 @@
parseSmallImage()!!,
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
).apply {
+ setCommon(this@toApiComplicationData)
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
- setCachedWireComplicationData(wireComplicationData)
- setDataSource(dataSourceCopy)
- setPersistencePolicy(persistencePolicyCopy)
- setDisplayPolicy(displayPolicyCopy)
}.build()
PhotoImageComplicationData.TYPE.toWireComplicationType() ->
@@ -2565,24 +2459,18 @@
largeImage!!,
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
).apply {
+ setCommon(this@toApiComplicationData)
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
- setCachedWireComplicationData(wireComplicationData)
- setDataSource(dataSourceCopy)
- setPersistencePolicy(persistencePolicyCopy)
- setDisplayPolicy(displayPolicyCopy)
}.build()
NoPermissionComplicationData.TYPE.toWireComplicationType() ->
NoPermissionComplicationData.Builder().apply {
+ setCommon(this@toApiComplicationData)
setMonochromaticImage(parseIcon())
setSmallImage(parseSmallImage())
setTitle(shortTitle?.toApiComplicationText())
setText(shortText?.toApiComplicationText())
- setCachedWireComplicationData(wireComplicationData)
- setDataSource(dataSourceCopy)
- setPersistencePolicy(persistencePolicyCopy)
- setDisplayPolicy(displayPolicyCopy)
}.build()
GoalProgressComplicationData.TYPE.toWireComplicationType() ->
@@ -2593,19 +2481,16 @@
contentDescription = contentDescription?.toApiComplicationText()
?: ComplicationText.EMPTY
).apply {
+ setCommon(this@toApiComplicationData)
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
setMonochromaticImage(parseIcon())
setSmallImage(parseSmallImage())
setTitle(shortTitle?.toApiComplicationText())
setText(shortText?.toApiComplicationText())
- setCachedWireComplicationData(wireComplicationData)
colorRamp?.let {
setColorRamp(ColorRamp(it, isColorRampInterpolated!!))
}
- setDataSource(dataSourceCopy)
- setPersistencePolicy(persistencePolicyCopy)
- setDisplayPolicy(displayPolicyCopy)
}.build()
WeightedElementsComplicationData.TYPE.toWireComplicationType() -> {
@@ -2620,6 +2505,7 @@
}.toList(),
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
).apply {
+ setCommon(this@toApiComplicationData)
setElementBackgroundColor(elementBackgroundColor)
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
@@ -2627,10 +2513,6 @@
setSmallImage(parseSmallImage())
setTitle(shortTitle?.toApiComplicationText())
setText(shortText?.toApiComplicationText())
- setCachedWireComplicationData(wireComplicationData)
- setDataSource(dataSourceCopy)
- setPersistencePolicy(persistencePolicyCopy)
- setDisplayPolicy(displayPolicyCopy)
}.build()
}
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Image.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Image.kt
index e965d2f..35bda85 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Image.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Image.kt
@@ -16,6 +16,7 @@
package androidx.wear.watchface.complications.data
+import android.support.wearable.complications.ComplicationData as WireComplicationData
import android.graphics.drawable.Icon
import android.os.Build
import androidx.annotation.RequiresApi
@@ -69,7 +70,7 @@
}
/** Adds a [MonochromaticImage] to a builder for [WireComplicationData]. */
- internal fun addToWireComplicationData(builder: WireComplicationDataBuilder) = builder.apply {
+ internal fun addToWireComplicationData(builder: WireComplicationData.Builder) = builder.apply {
setIcon(image)
setBurnInProtectionIcon(ambientImage)
}
@@ -173,7 +174,7 @@
}
/** Adds a [SmallImage] to a builder for [WireComplicationData]. */
- internal fun addToWireComplicationData(builder: WireComplicationDataBuilder) = builder.apply {
+ internal fun addToWireComplicationData(builder: WireComplicationData.Builder) = builder.apply {
setSmallImage(image)
setSmallImageStyle(
when (this@SmallImage.type) {
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
index f318733..5887dd6 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
@@ -16,6 +16,11 @@
package androidx.wear.watchface.complications.data
+import android.support.wearable.complications.ComplicationData as WireComplicationData
+import android.support.wearable.complications.ComplicationText as WireComplicationText
+import android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder as WireComplicationTextTimeDifferenceBuilder
+import android.support.wearable.complications.ComplicationText.TimeFormatBuilder as WireComplicationTextTimeFormatBuilder
+import android.support.wearable.complications.TimeDependentText as WireTimeDependentText
import android.content.res.Resources
import android.icu.util.TimeZone
import android.support.wearable.complications.TimeDependentText
@@ -32,17 +37,6 @@
import java.time.Instant
import java.util.concurrent.TimeUnit
-/** The wire format for [ComplicationText]. */
-internal typealias WireComplicationText = android.support.wearable.complications.ComplicationText
-
-private typealias WireComplicationTextTimeDifferenceBuilder =
- android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder
-
-private typealias WireComplicationTextTimeFormatBuilder =
- android.support.wearable.complications.ComplicationText.TimeFormatBuilder
-
-private typealias WireTimeDependentText = android.support.wearable.complications.TimeDependentText
-
@JvmDefaultWithCompatibility
/**
* The text within a complication.
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Time.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Time.kt
index d479418..c402f16 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Time.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Time.kt
@@ -16,6 +16,7 @@
package androidx.wear.watchface.complications.data
+import android.support.wearable.complications.ComplicationData as WireComplicationData
import java.time.Instant
/** A range of time, that may be unbounded on either side. */
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
index 49b6a23..f4dfa77 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
@@ -16,6 +16,7 @@
package androidx.wear.watchface.complications.data
+import android.support.wearable.complications.ComplicationData as WireComplicationData
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
index 5643563..a3019ad 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
@@ -16,6 +16,7 @@
package androidx.wear.watchface.complications.data
+import android.support.wearable.complications.ComplicationData as WireComplicationData
import android.util.Log
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.any
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
index 7b51af9..5ca6603 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
@@ -18,6 +18,8 @@
package androidx.wear.watchface.complications.data
+import android.support.wearable.complications.ComplicationData as WireComplicationData
+import android.support.wearable.complications.ComplicationText as WireComplicationText
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.ComponentName
@@ -66,7 +68,7 @@
val data = NoDataComplicationData()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(null)
.setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
.setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
@@ -90,7 +92,7 @@
val data = EmptyComplicationData()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_EMPTY).build()
+ WireComplicationData.Builder(WireComplicationData.TYPE_EMPTY).build()
)
testRoundTripConversions(data)
assertThat(serializeAndDeserialize(data)).isInstanceOf(EmptyComplicationData::class.java)
@@ -105,7 +107,7 @@
val data = NotConfiguredComplicationData()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NOT_CONFIGURED).build()
+ WireComplicationData.Builder(WireComplicationData.TYPE_NOT_CONFIGURED).build()
)
testRoundTripConversions(data)
assertThat(serializeAndDeserialize(data))
@@ -129,7 +131,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
.setShortText(WireComplicationText.plainText("text"))
.setShortTitle(WireComplicationText.plainText("title"))
.setContentDescription(WireComplicationText.plainText("content description"))
@@ -174,7 +176,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
.setShortText(WireComplicationText.plainText("text"))
.setShortTitle(WireComplicationText.plainText("title"))
.setIcon(monochromaticImageIcon)
@@ -226,7 +228,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
.setLongText(WireComplicationText.plainText("text"))
.setLongTitle(WireComplicationText.plainText("title"))
.setContentDescription(WireComplicationText.plainText("content description"))
@@ -271,7 +273,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
.setLongText(WireComplicationText.plainText("text"))
.setLongTitle(WireComplicationText.plainText("title"))
.setIcon(monochromaticImageIcon)
@@ -324,7 +326,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(95f)
.setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
.setRangedMinValue(0f)
@@ -374,7 +376,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
.setRangedValue(5f) // min as a sensible default
.setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
@@ -426,7 +428,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(95f)
.setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
.setRangedMinValue(0f)
@@ -478,7 +480,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(95f)
.setRangedMinValue(0f)
.setRangedMaxValue(100f)
@@ -536,7 +538,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValue(1200f)
.setTargetValue(10000f)
.setShortTitle(WireComplicationText.plainText("steps"))
@@ -582,7 +584,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
.setRangedValue(0f) // sensible default
.setTargetValue(10000f)
@@ -630,7 +632,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValue(1200f)
.setTargetValue(10000f)
.setShortTitle(WireComplicationText.plainText("steps"))
@@ -680,7 +682,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValue(1200f)
.setTargetValue(10000f)
.setIcon(monochromaticImageIcon)
@@ -738,7 +740,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(95f)
.setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
.setRangedMinValue(0f)
@@ -807,7 +809,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
.setElementBackgroundColor(Color.GRAY)
@@ -864,7 +866,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
.setElementBackgroundColor(Color.TRANSPARENT)
@@ -922,7 +924,7 @@
).setDataSource(dataSource).build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+ WireComplicationData.Builder(WireComplicationData.TYPE_ICON)
.setIcon(icon)
.setContentDescription(WireComplicationText.plainText("content description"))
.setDataSource(dataSource)
@@ -957,7 +959,7 @@
).setDataSource(dataSource).build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_SMALL_IMAGE)
.setSmallImage(icon)
.setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
.setContentDescription(WireComplicationText.plainText("content description"))
@@ -997,7 +999,7 @@
).setDataSource(dataSource).build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_SMALL_IMAGE)
.setSmallImage(bitmapIcon)
.setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
.setContentDescription(WireComplicationText.plainText("content description"))
@@ -1028,7 +1030,7 @@
).setDataSource(dataSource).build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_LARGE_IMAGE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_LARGE_IMAGE)
.setLargeImage(photoImage)
.setContentDescription(WireComplicationText.plainText("content description"))
.setDataSource(dataSource)
@@ -1062,7 +1064,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_PERMISSION)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_PERMISSION)
.setShortText(WireComplicationText.plainText("needs location"))
.setDataSource(dataSource)
.setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
@@ -1095,7 +1097,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_PERMISSION)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_PERMISSION)
.setShortText(WireComplicationText.plainText("needs location"))
.setIcon(monochromaticImageIcon)
.setSmallImage(smallImageIcon)
@@ -1137,9 +1139,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
.setShortTitle(ComplicationText.PLACEHOLDER.toWireComplicationText())
.setIcon(createPlaceholderIcon())
@@ -1190,9 +1192,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
.setLongText(WireComplicationText.plainText("text"))
.setContentDescription(
WireComplicationText.plainText("content description")
@@ -1243,9 +1245,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(RangedValueComplicationData.PLACEHOLDER)
.setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
.setRangedMinValue(0f)
@@ -1301,9 +1303,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValue(GoalProgressComplicationData.PLACEHOLDER)
.setTargetValue(10000f)
.setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
@@ -1364,9 +1366,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
.setElementBackgroundColor(Color.GRAY)
@@ -1424,9 +1426,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(RangedValueComplicationData.PLACEHOLDER)
.setRangedValueType(RangedValueComplicationData.TYPE_RATING)
.setRangedMinValue(0f)
@@ -1480,9 +1482,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+ WireComplicationData.Builder(WireComplicationData.TYPE_ICON)
.setIcon(createPlaceholderIcon())
.setContentDescription(
WireComplicationText.plainText("content description")
@@ -1527,9 +1529,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_SMALL_IMAGE)
.setSmallImage(createPlaceholderIcon())
.setSmallImageStyle(WireComplicationData.IMAGE_STYLE_ICON)
.setContentDescription(
@@ -1575,9 +1577,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_LARGE_IMAGE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_LARGE_IMAGE)
.setLargeImage(createPlaceholderIcon())
.setContentDescription(
WireComplicationText.plainText("content description")
@@ -1639,7 +1641,7 @@
@Test
public fun noDataComplicationData() {
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(null)
.setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
.setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
@@ -1651,7 +1653,7 @@
@Test
public fun emptyComplicationData() {
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_EMPTY).build(),
+ WireComplicationData.Builder(WireComplicationData.TYPE_EMPTY).build(),
ComplicationType.EMPTY
)
}
@@ -1659,7 +1661,7 @@
@Test
public fun notConfiguredComplicationData() {
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NOT_CONFIGURED).build(),
+ WireComplicationData.Builder(WireComplicationData.TYPE_NOT_CONFIGURED).build(),
ComplicationType.NOT_CONFIGURED
)
}
@@ -1667,7 +1669,7 @@
@Test
public fun shortTextComplicationData() {
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
.setShortText(WireComplicationText.plainText("text"))
.setShortTitle(WireComplicationText.plainText("title"))
.setContentDescription(WireComplicationText.plainText("content description"))
@@ -1681,7 +1683,7 @@
@Test
public fun longTextComplicationData() {
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
.setLongText(WireComplicationText.plainText("text"))
.setLongTitle(WireComplicationText.plainText("title"))
.setContentDescription(WireComplicationText.plainText("content description"))
@@ -1695,7 +1697,7 @@
@Test
public fun rangedValueComplicationData_withFixedValue() {
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(95f)
.setRangedMinValue(0f)
.setRangedMaxValue(100f)
@@ -1711,7 +1713,7 @@
@Test
public fun rangedValueComplicationData_withValueExpression() {
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
.setRangedMinValue(0f)
.setRangedMaxValue(100f)
@@ -1727,7 +1729,7 @@
@Test
public fun rangedValueComplicationData_drawSegmented() {
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(95f)
.setRangedMinValue(0f)
.setRangedMaxValue(100f)
@@ -1743,7 +1745,7 @@
@Test
public fun goalProgressComplicationData_withFixedValue() {
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValue(1200f)
.setTargetValue(10000f)
.setShortTitle(WireComplicationText.plainText("steps"))
@@ -1760,7 +1762,7 @@
@Test
public fun goalProgressComplicationData_withValueExpression() {
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
.setTargetValue(10000f)
.setShortTitle(WireComplicationText.plainText("steps"))
@@ -1777,7 +1779,7 @@
@Test
public fun weightedElementsComplicationData() {
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
.setElementBackgroundColor(Color.DKGRAY)
@@ -1794,7 +1796,7 @@
public fun monochromaticImageComplicationData() {
val icon = Icon.createWithContentUri("someuri")
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+ WireComplicationData.Builder(WireComplicationData.TYPE_ICON)
.setIcon(icon)
.setContentDescription(WireComplicationText.plainText("content description"))
.setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
@@ -1808,7 +1810,7 @@
public fun smallImageComplicationData() {
val icon = Icon.createWithContentUri("someuri")
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_SMALL_IMAGE)
.setSmallImage(icon)
.setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
.setContentDescription(WireComplicationText.plainText("content description"))
@@ -1823,7 +1825,7 @@
public fun photoImageComplicationData() {
val icon = Icon.createWithContentUri("someuri")
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_LARGE_IMAGE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_LARGE_IMAGE)
.setLargeImage(icon)
.setContentDescription(WireComplicationText.plainText("content description"))
.setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
@@ -1836,7 +1838,7 @@
@Test
public fun noPermissionComplicationData() {
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_PERMISSION)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_PERMISSION)
.setShortText(WireComplicationText.plainText("needs location"))
.setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
.setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
@@ -1849,9 +1851,9 @@
public fun noDataComplicationData_shortText() {
val icon = Icon.createWithContentUri("someuri")
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
.setContentDescription(
WireComplicationText.plainText("content description")
)
@@ -1873,9 +1875,9 @@
public fun noDataComplicationData_longText() {
val icon = Icon.createWithContentUri("someuri")
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
.setContentDescription(
WireComplicationText.plainText("content description")
)
@@ -1897,9 +1899,9 @@
public fun noDataComplicationData_rangedValue() {
val icon = Icon.createWithContentUri("someuri")
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
.setContentDescription(
WireComplicationText.plainText("content description")
)
@@ -1924,9 +1926,9 @@
public fun noDataComplicationData_goalProgress() {
val icon = Icon.createWithContentUri("someuri")
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValue(1200f)
.setTargetValue(10000f)
.setShortTitle(WireComplicationText.plainText("steps"))
@@ -1950,9 +1952,9 @@
@Test
public fun noDataComplicationData_weightedElementsComplicationData() {
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
.setElementBackgroundColor(Color.DKGRAY)
@@ -1975,9 +1977,9 @@
public fun noDataComplicationData_smallImage() {
val icon = Icon.createWithContentUri("someuri")
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_SMALL_IMAGE)
.setSmallImage(icon)
.setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
.setContentDescription(
@@ -1998,9 +2000,9 @@
public fun noDataComplicationData_monochromaticImage() {
val icon = Icon.createWithContentUri("someuri")
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+ WireComplicationData.Builder(WireComplicationData.TYPE_ICON)
.setIcon(icon)
.setContentDescription(
WireComplicationText.plainText("content description")
@@ -2285,7 +2287,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
.setShortText(WireComplicationText.plainText("text"))
.setStartDateTimeMillis(testStartInstant.toEpochMilli())
.setEndDateTimeMillis(testEndDateInstant.toEpochMilli())
@@ -2302,7 +2304,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
.setLongText(WireComplicationText.plainText("text"))
.setStartDateTimeMillis(testStartInstant.toEpochMilli())
.setEndDateTimeMillis(testEndDateInstant.toEpochMilli())
@@ -2323,7 +2325,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(95f)
.setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
.setRangedMinValue(0f)
@@ -2349,7 +2351,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValue(1200f)
.setTargetValue(10000f)
.setShortTitle(WireComplicationText.plainText("steps"))
@@ -2379,7 +2381,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
.setElementBackgroundColor(Color.TRANSPARENT)
@@ -2402,7 +2404,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+ WireComplicationData.Builder(WireComplicationData.TYPE_ICON)
.setIcon(icon)
.setStartDateTimeMillis(testStartInstant.toEpochMilli())
.setEndDateTimeMillis(testEndDateInstant.toEpochMilli())
@@ -2421,7 +2423,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_SMALL_IMAGE)
.setSmallImage(icon)
.setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
.setStartDateTimeMillis(testStartInstant.toEpochMilli())
@@ -2440,7 +2442,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_LARGE_IMAGE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_LARGE_IMAGE)
.setLargeImage(photoImage)
.setStartDateTimeMillis(testStartInstant.toEpochMilli())
.setEndDateTimeMillis(testEndDateInstant.toEpochMilli())
@@ -2458,9 +2460,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
.setShortText(WireComplicationText.plainText("text"))
.setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
.setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
@@ -2480,9 +2482,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
.setLongText(WireComplicationText.plainText("text"))
.setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
.setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
@@ -2508,9 +2510,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
.setRangedValue(95f)
.setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
.setRangedMinValue(0f)
@@ -2539,9 +2541,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValue(1200f)
.setTargetValue(10000f)
.setShortTitle(WireComplicationText.plainText("steps"))
@@ -2576,9 +2578,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
+ WireComplicationData.Builder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
.setElementBackgroundColor(Color.TRANSPARENT)
@@ -2605,9 +2607,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+ WireComplicationData.Builder(WireComplicationData.TYPE_ICON)
.setIcon(icon)
.setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
.setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
@@ -2628,9 +2630,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_SMALL_IMAGE)
.setSmallImage(icon)
.setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
.setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
@@ -2651,9 +2653,9 @@
)
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.TYPE_LARGE_IMAGE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_LARGE_IMAGE)
.setLargeImage(icon)
.setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
.setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
index 513af6b..a77abb9 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
@@ -425,7 +425,7 @@
timelineEntry.timelineStartEpochSecond = 100
timelineEntry.timelineEndEpochSecond = 1000
- val wireLongTextComplication = WireComplicationDataBuilder(
+ val wireLongTextComplication = ComplicationData.Builder(
ComplicationType.LONG_TEXT.toWireComplicationType()
)
.setEndDateTimeMillis(1650988800000)
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt
index a2ece5e..492d983 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt
@@ -16,6 +16,9 @@
package androidx.wear.watchface.complications.data
+import android.support.wearable.complications.ComplicationText as WireComplicationText
+import android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder as WireTimeDifferenceBuilder
+import android.support.wearable.complications.ComplicationText.TimeFormatBuilder as WireTimeFormatBuilder
import android.content.Context
import android.icu.util.TimeZone
import android.support.wearable.complications.ComplicationText
@@ -28,12 +31,6 @@
import java.time.Instant
import java.util.concurrent.TimeUnit
-private typealias WireTimeDifferenceBuilder =
- android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder
-
-private typealias WireTimeFormatBuilder =
- android.support.wearable.complications.ComplicationText.TimeFormatBuilder
-
@RunWith(SharedRobolectricTestRunner::class)
public class AsWireComplicationTextTest {
@Test
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TypeTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TypeTest.kt
index 4c4b0a9..1e2a28c 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TypeTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TypeTest.kt
@@ -16,6 +16,7 @@
package androidx.wear.watchface.complications.data
+import android.support.wearable.complications.ComplicationData as WireComplicationData
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/wear/watchface/watchface-complications/src/androidTest/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetrieverTest.kt b/wear/watchface/watchface-complications/src/androidTest/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetrieverTest.kt
index da8530c..230310b 100644
--- a/wear/watchface/watchface-complications/src/androidTest/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetrieverTest.kt
+++ b/wear/watchface/watchface-complications/src/androidTest/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetrieverTest.kt
@@ -16,6 +16,7 @@
package androidx.wear.watchface.complications
+import android.support.wearable.complications.ComplicationProviderInfo as WireComplicationProviderInfo
import android.app.Service
import android.content.ComponentName
import android.content.Context
@@ -41,9 +42,6 @@
import org.junit.runner.RunWith
import java.time.Instant
-private typealias WireComplicationProviderInfo =
- android.support.wearable.complications.ComplicationProviderInfo
-
private const val LEFT_COMPLICATION_ID = 101
private const val RIGHT_COMPLICATION_ID = 102
diff --git a/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetriever.kt b/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetriever.kt
index 99d37bb..dbc4246 100644
--- a/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetriever.kt
+++ b/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetriever.kt
@@ -15,6 +15,7 @@
*/
package androidx.wear.watchface.complications
+import android.support.wearable.complications.ComplicationProviderInfo as WireComplicationProviderInfo
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
@@ -53,9 +54,6 @@
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.CancellableContinuation
-private typealias WireComplicationProviderInfo =
- android.support.wearable.complications.ComplicationProviderInfo
-
/**
* Retrieves [Result] for a watch face's complications.
*
@@ -440,14 +438,11 @@
)
}
-// Ugh we need this since the linter wants the method signature all on one line...
-typealias ApiInfo = ComplicationDataSourceInfo
-
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public fun WireComplicationProviderInfo.toApiComplicationDataSourceInfo(): ApiInfo =
+public fun WireComplicationProviderInfo.toApiComplicationDataSourceInfo() =
ComplicationDataSourceInfo(
appName!!, providerName!!, providerIcon!!, fromWireType(complicationType),
providerComponentName
diff --git a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
index af75b55..d3e8b68 100644
--- a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
+++ b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
@@ -17,7 +17,6 @@
package androidx.wear.watchface.control.data;
import android.annotation.SuppressLint;
-import android.content.ComponentName;
import android.os.Parcel;
import android.os.Parcelable;
@@ -92,16 +91,15 @@
@NonNull WatchUiState watchUiState,
@NonNull UserStyleWireFormat userStyle,
@Nullable List<IdAndComplicationDataWireFormat> idAndComplicationDataWireFormats,
- @Nullable ComponentName auxiliaryComponentName) {
+ @Nullable String auxiliaryComponentPackageName,
+ @Nullable String auxiliaryComponentClassName) {
mInstanceId = instanceId;
mDeviceConfig = deviceConfig;
mWatchUiState = watchUiState;
mUserStyle = userStyle;
mIdAndComplicationDataWireFormats = idAndComplicationDataWireFormats;
- if (auxiliaryComponentName != null) {
- mAuxiliaryComponentClassName = auxiliaryComponentName.getClassName();
- mAuxiliaryComponentPackageName = auxiliaryComponentName.getPackageName();
- }
+ mAuxiliaryComponentClassName = auxiliaryComponentClassName;
+ mAuxiliaryComponentPackageName = auxiliaryComponentPackageName;
}
@NonNull
diff --git a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
index f8818fe..fb6ed5da 100644
--- a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
+++ b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
@@ -16,6 +16,7 @@
package androidx.wear.watchface.editor
+import android.support.wearable.complications.ComplicationProviderInfo as WireComplicationProviderInfo
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ComponentName
@@ -145,9 +146,6 @@
private const val PROVIDER_CHOOSER_RESULT_EXTRA_KEY = "PROVIDER_CHOOSER_RESULT_EXTRA_KEY"
private const val PROVIDER_CHOOSER_RESULT_EXTRA_VALUE = "PROVIDER_CHOOSER_RESULT_EXTRA_VALUE"
-private typealias WireComplicationProviderInfo =
- android.support.wearable.complications.ComplicationProviderInfo
-
internal val redStyleOption = ListOption(Option.Id("red_style"), "Red", "Red", icon = null)
internal val greenStyleOption =
ListOption(Option.Id("green_style"), "Green", "Green", icon = null)
diff --git a/wear/watchface/watchface-style/old-api-test-service/build.gradle b/wear/watchface/watchface-style/old-api-test-service/build.gradle
index b14ceb0..6d668ff 100644
--- a/wear/watchface/watchface-style/old-api-test-service/build.gradle
+++ b/wear/watchface/watchface-style/old-api-test-service/build.gradle
@@ -75,7 +75,7 @@
}
androidComponents {
- onVariants(selector().all(), { variant ->
+ onVariants(selector().all().withBuildType("release"), { variant ->
artifacts {
apkAssets(variant.artifacts.get(SingleArtifact.APK.INSTANCE))
}
diff --git a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/OldClientAidlCompatTest.kt b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/OldClientAidlCompatTest.kt
index 38680ef..5cf9133 100644
--- a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/OldClientAidlCompatTest.kt
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/OldClientAidlCompatTest.kt
@@ -39,6 +39,7 @@
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -191,6 +192,7 @@
}
@Test
+ @Ignore // TODO(b/265425077): This test is failing on the bots, fix it.
fun roundTripUserStyleSchema() = runBlocking {
val service = IStyleEchoService.Stub.asInterface(
bindService(Intent(ACTON).apply { setPackage(PACKAGE_NAME) })
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
index 60c9f35..93526dc 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
@@ -283,6 +283,7 @@
WatchUiState(false, 0),
UserStyleWireFormat(emptyMap()),
null,
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
@@ -855,6 +856,7 @@
mapOf(COLOR_STYLE_SETTING to GREEN_STYLE.encodeToByteArray())
),
null,
+ null,
null
),
null
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/XmlDefinedUserStyleSchemaAndComplicationSlotsTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/XmlDefinedUserStyleSchemaAndComplicationSlotsTest.kt
index 24d49ce..1da812c 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/XmlDefinedUserStyleSchemaAndComplicationSlotsTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/XmlDefinedUserStyleSchemaAndComplicationSlotsTest.kt
@@ -198,6 +198,7 @@
WatchUiState(false, 0),
UserStyleWireFormat(emptyMap()),
null,
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index 88fa276..7d5ff70 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -16,6 +16,7 @@
package androidx.wear.watchface
+import android.support.wearable.complications.ComplicationData as WireComplicationData
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.RectF
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 8c1ff64..b3ee562 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -16,6 +16,7 @@
package androidx.wear.watchface
+import android.support.wearable.complications.ComplicationData as WireComplicationData
import android.app.KeyguardManager
import android.content.ComponentName
import android.content.Context
@@ -111,9 +112,6 @@
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
-/** The wire format for [ComplicationData]. */
-internal typealias WireComplicationData = android.support.wearable.complications.ComplicationData
-
/**
* After user code finishes, we need up to 100ms of wake lock holding for the drawing to occur. This
* isn't the ideal approach, but the framework doesn't expose a callback that would tell us when our
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
index cfad995..c09f23b 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
@@ -133,6 +133,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index b12863b..d160094 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -625,6 +625,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
),
complicationCache: MutableMap<String, ByteArray>? = null,
@@ -1761,6 +1762,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
)
@@ -1795,6 +1797,7 @@
)
).toWireFormat(),
null,
+ null,
null
)
)
@@ -1828,6 +1831,7 @@
WatchUiState(false, 0),
UserStyle(mapOf(watchHandStyleSetting to badStyleOption)).toWireFormat(),
null,
+ null,
null
)
)
@@ -1886,6 +1890,7 @@
WatchUiState(false, 0),
UserStyle(mapOf(watchHandStyleSetting to badStyleOption)).toWireFormat(),
null,
+ null,
null
)
)
@@ -1908,6 +1913,7 @@
WatchUiState(false, 0),
UserStyle(mapOf(watchHandStyleSetting to badStyleOption)).toWireFormat(),
null,
+ null,
null
)
)
@@ -2334,6 +2340,7 @@
)
).toWireFormat(),
null,
+ null,
null
)
@@ -2366,6 +2373,7 @@
)
).toWireFormat(),
null,
+ null,
null
)
@@ -2492,6 +2500,7 @@
set(complicationsStyleSetting, leftOnlyComplicationsOption)
}.toUserStyle().toWireFormat(),
emptyList(),
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
@@ -2816,6 +2825,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
)
@@ -2876,6 +2886,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
)
@@ -2930,6 +2941,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
initWallpaperInteractiveWatchFaceInstance(
@@ -3061,6 +3073,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
initWallpaperInteractiveWatchFaceInstance(
@@ -3184,6 +3197,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
initWallpaperInteractiveWatchFaceInstance(
@@ -3355,6 +3369,7 @@
)
).toWireFormat(),
null,
+ null,
null
),
choreographer
@@ -3405,6 +3420,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
)
@@ -3473,6 +3489,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
)
@@ -3521,6 +3538,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
)
@@ -3549,6 +3567,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
)
@@ -3585,6 +3604,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
)
@@ -3673,6 +3693,7 @@
WatchUiState(true, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
)
@@ -3719,6 +3740,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
)
@@ -3759,6 +3781,7 @@
WatchUiState(false, 0),
UserStyle(hashMapOf(colorStyleSetting to blueStyleOption)).toWireFormat(),
null,
+ null,
null
)
)
@@ -3820,6 +3843,7 @@
.build()
)
),
+ null,
null
)
)
@@ -3878,6 +3902,7 @@
)
).toWireFormat(),
null,
+ null,
null
),
choreographer
@@ -3933,6 +3958,7 @@
)
).toWireFormat(),
null,
+ null,
null
),
choreographer
@@ -3972,6 +3998,7 @@
.build()
)
),
+ null,
null
)
)
@@ -4055,6 +4082,7 @@
.build()
)
),
+ null,
null
)
)
@@ -4213,6 +4241,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
@@ -4267,6 +4296,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
),
)
@@ -4298,6 +4328,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
),
)
@@ -4330,6 +4361,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
)
)
@@ -4422,6 +4454,7 @@
)
).toWireFormat(),
null,
+ null,
null
)
)
@@ -4469,6 +4502,7 @@
WatchUiState(false, 0),
UserStyle(hashMapOf(watchHandStyleSetting to gothicStyleOption)).toWireFormat(),
null,
+ null,
null
)
)
@@ -4500,6 +4534,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
)
)
@@ -4602,6 +4637,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
@@ -4741,6 +4777,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
@@ -4872,6 +4909,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
@@ -4975,6 +5013,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
@@ -5026,6 +5065,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
)
)
@@ -5117,6 +5157,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
@@ -5194,6 +5235,7 @@
)
).toWireFormat(),
null,
+ null,
null
),
choreographer
@@ -5265,6 +5307,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
),
choreographer
@@ -5462,6 +5505,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
)
@@ -5504,6 +5548,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
)
@@ -5641,6 +5686,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
@@ -5712,6 +5758,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
@@ -5783,6 +5830,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
@@ -5861,6 +5909,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
@@ -5964,6 +6013,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
)
@@ -6022,6 +6072,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
)
@@ -6115,6 +6166,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
@@ -6234,6 +6286,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
@@ -6358,6 +6411,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList(),
+ null,
null
),
object : IPendingInteractiveWatchFace.Stub() {
@@ -6490,6 +6544,7 @@
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null,
+ null,
null
)
)
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParamsTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParamsTest.kt
index 19549fb..783b044 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParamsTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParamsTest.kt
@@ -15,7 +15,6 @@
*/
package androidx.wear.watchface.control.data
-import android.content.ComponentName
import androidx.versionedparcelable.ParcelUtils
import androidx.wear.watchface.data.DeviceConfig
import androidx.wear.watchface.data.WatchUiState
@@ -40,7 +39,8 @@
),
UserStyle(emptyMap()).toWireFormat(),
null,
- ComponentName("some.package", "SomeClass")
+ null,
+ null
)
val dummyOutputStream = ByteArrayOutputStream()
diff --git a/wear/wear-samples-ambient/build.gradle b/wear/wear-samples-ambient/build.gradle
index 1cf4e6a..b73c613 100644
--- a/wear/wear-samples-ambient/build.gradle
+++ b/wear/wear-samples-ambient/build.gradle
@@ -24,8 +24,8 @@
dependencies {
api(project(":wear:wear"))
+ implementation("androidx.core:core:1.6.0")
implementation(libs.kotlinStdlib)
-
}
androidx {
diff --git a/wear/wear-samples-ambient/src/main/java/androidx/wear/samples/ambient/MainActivity.kt b/wear/wear-samples-ambient/src/main/java/androidx/wear/samples/ambient/MainActivity.kt
index 0b9ead7..1c8e558 100644
--- a/wear/wear-samples-ambient/src/main/java/androidx/wear/samples/ambient/MainActivity.kt
+++ b/wear/wear-samples-ambient/src/main/java/androidx/wear/samples/ambient/MainActivity.kt
@@ -5,15 +5,16 @@
import android.os.Looper
import android.util.Log
import android.widget.TextView
-import androidx.fragment.app.FragmentActivity
-import androidx.wear.ambient.AmbientModeSupport
+import androidx.core.app.ComponentActivity
+import androidx.wear.ambient.AmbientLifecycleObserver
+import androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientDetails
+import androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientLifecycleCallback
import java.text.SimpleDateFormat
import java.util.Date
/** Sample activity that provides an ambient experience. */
class MainActivity :
- FragmentActivity(R.layout.activity_main),
- AmbientModeSupport.AmbientCallbackProvider {
+ ComponentActivity() {
/** Used to dispatch periodic updates when the activity is in active mode. */
private val activeUpdatesHandler = Handler(Looper.getMainLooper())
@@ -22,7 +23,7 @@
private val model = MainViewModel()
/** The controller for ambient mode, initialized when the activity is created. */
- private lateinit var ambientController: AmbientModeSupport.AmbientController
+ private lateinit var ambientObserver: AmbientLifecycleObserver
// The views that are part of the activity.
private val timerTextView by lazy { findViewById<TextView>(R.id.timer) }
@@ -30,11 +31,31 @@
private val timestampTextView by lazy { findViewById<TextView>(R.id.timestamp) }
private val updatesTextView by lazy { findViewById<TextView>(R.id.updates) }
+ private val ambientCallback = object : AmbientLifecycleCallback {
+ override fun onEnterAmbient(ambientDetails: AmbientDetails) {
+ Log.d(TAG, "onEnterAmbient()")
+ model.setStatus(Status.AMBIENT)
+ model.publishUpdate()
+ }
+
+ override fun onUpdateAmbient() {
+ Log.d(TAG, "onUpdateAmbient()")
+ model.publishUpdate()
+ }
+
+ override fun onExitAmbient() {
+ Log.d(TAG, "onExitAmbient()")
+ model.setStatus(Status.ACTIVE)
+ model.publishUpdate()
+ schedule()
+ }
+ }
+
/** Invoked on [activeUpdatesHandler], posts an update when the activity is in active mode. */
private val mActiveUpdatesRunnable: Runnable =
Runnable {
// If invoked in ambient mode, do nothing.
- if (ambientController.isAmbient) {
+ if (ambientObserver.isAmbient()) {
return@Runnable
}
model.publishUpdate()
@@ -45,8 +66,10 @@
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate")
+ setContentView(R.layout.activity_main)
observeModel()
- ambientController = AmbientModeSupport.attach(this)
+ ambientObserver = AmbientLifecycleObserver(this, ambientCallback)
+ this.lifecycle.addObserver(ambientObserver)
}
override fun onStart() {
@@ -78,29 +101,6 @@
super.onDestroy()
}
- override fun getAmbientCallback() = object : AmbientModeSupport.AmbientCallback() {
- override fun onEnterAmbient(ambientDetails: Bundle) {
- super.onEnterAmbient(ambientDetails)
- Log.d(TAG, "onEnterAmbient()")
- model.setStatus(Status.AMBIENT)
- model.publishUpdate()
- }
-
- override fun onUpdateAmbient() {
- super.onUpdateAmbient()
- Log.d(TAG, "onUpdateAmbient()")
- model.publishUpdate()
- }
-
- override fun onExitAmbient() {
- super.onExitAmbient()
- Log.d(TAG, "onExitAmbient()")
- model.setStatus(Status.ACTIVE)
- model.publishUpdate()
- schedule()
- }
- }
-
private fun observeModel() {
model.observeStartTime(this) {
timerTextView.text = formatTimer(model.getTimer())
diff --git a/wear/wear/api/current.txt b/wear/wear/api/current.txt
index 30a2fbd..b1e72e0 100644
--- a/wear/wear/api/current.txt
+++ b/wear/wear/api/current.txt
@@ -17,6 +17,30 @@
package androidx.wear.ambient {
+ public final class AmbientLifecycleObserver implements androidx.wear.ambient.AmbientLifecycleObserverInterface {
+ ctor public AmbientLifecycleObserver(android.app.Activity activity, java.util.concurrent.Executor callbackExecutor, androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientLifecycleCallback callbacks);
+ ctor public AmbientLifecycleObserver(android.app.Activity activity, androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientLifecycleCallback callbacks);
+ method public boolean isAmbient();
+ }
+
+ public interface AmbientLifecycleObserverInterface extends androidx.lifecycle.DefaultLifecycleObserver {
+ method public boolean isAmbient();
+ }
+
+ public static final class AmbientLifecycleObserverInterface.AmbientDetails {
+ ctor public AmbientLifecycleObserverInterface.AmbientDetails(boolean burnInProtectionRequired, boolean deviceHasLowBitAmbient);
+ method public boolean getBurnInProtectionRequired();
+ method public boolean getDeviceHasLowBitAmbient();
+ property public final boolean burnInProtectionRequired;
+ property public final boolean deviceHasLowBitAmbient;
+ }
+
+ public static interface AmbientLifecycleObserverInterface.AmbientLifecycleCallback {
+ method public default void onEnterAmbient(androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientDetails ambientDetails);
+ method public default void onExitAmbient();
+ method public default void onUpdateAmbient();
+ }
+
@Deprecated public final class AmbientMode extends android.app.Fragment {
ctor @Deprecated public AmbientMode();
method @Deprecated public static <T extends android.app.Activity> androidx.wear.ambient.AmbientMode.AmbientController! attachAmbientSupport(T!);
@@ -50,30 +74,30 @@
method @Deprecated public void setAmbientOffloadEnabled(boolean);
}
- public final class AmbientModeSupport extends androidx.fragment.app.Fragment {
- ctor public AmbientModeSupport();
- method public static <T extends androidx.fragment.app.FragmentActivity> androidx.wear.ambient.AmbientModeSupport.AmbientController! attach(T!);
- field public static final String EXTRA_BURN_IN_PROTECTION = "com.google.android.wearable.compat.extra.BURN_IN_PROTECTION";
- field public static final String EXTRA_LOWBIT_AMBIENT = "com.google.android.wearable.compat.extra.LOWBIT_AMBIENT";
- field public static final String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode";
+ @Deprecated public final class AmbientModeSupport extends androidx.fragment.app.Fragment {
+ ctor @Deprecated public AmbientModeSupport();
+ method @Deprecated public static <T extends androidx.fragment.app.FragmentActivity> androidx.wear.ambient.AmbientModeSupport.AmbientController! attach(T!);
+ field @Deprecated public static final String EXTRA_BURN_IN_PROTECTION = "com.google.android.wearable.compat.extra.BURN_IN_PROTECTION";
+ field @Deprecated public static final String EXTRA_LOWBIT_AMBIENT = "com.google.android.wearable.compat.extra.LOWBIT_AMBIENT";
+ field @Deprecated public static final String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode";
}
- public abstract static class AmbientModeSupport.AmbientCallback {
- ctor public AmbientModeSupport.AmbientCallback();
- method public void onAmbientOffloadInvalidated();
- method public void onEnterAmbient(android.os.Bundle!);
- method public void onExitAmbient();
- method public void onUpdateAmbient();
+ @Deprecated public abstract static class AmbientModeSupport.AmbientCallback {
+ ctor @Deprecated public AmbientModeSupport.AmbientCallback();
+ method @Deprecated public void onAmbientOffloadInvalidated();
+ method @Deprecated public void onEnterAmbient(android.os.Bundle!);
+ method @Deprecated public void onExitAmbient();
+ method @Deprecated public void onUpdateAmbient();
}
- public static interface AmbientModeSupport.AmbientCallbackProvider {
- method public androidx.wear.ambient.AmbientModeSupport.AmbientCallback! getAmbientCallback();
+ @Deprecated public static interface AmbientModeSupport.AmbientCallbackProvider {
+ method @Deprecated public androidx.wear.ambient.AmbientModeSupport.AmbientCallback! getAmbientCallback();
}
- public final class AmbientModeSupport.AmbientController {
- method public boolean isAmbient();
- method public void setAmbientOffloadEnabled(boolean);
- method public void setAutoResumeEnabled(boolean);
+ @Deprecated public final class AmbientModeSupport.AmbientController {
+ method @Deprecated public boolean isAmbient();
+ method @Deprecated public void setAmbientOffloadEnabled(boolean);
+ method @Deprecated public void setAutoResumeEnabled(boolean);
}
}
diff --git a/wear/wear/api/public_plus_experimental_current.txt b/wear/wear/api/public_plus_experimental_current.txt
index 30a2fbd..b1e72e0 100644
--- a/wear/wear/api/public_plus_experimental_current.txt
+++ b/wear/wear/api/public_plus_experimental_current.txt
@@ -17,6 +17,30 @@
package androidx.wear.ambient {
+ public final class AmbientLifecycleObserver implements androidx.wear.ambient.AmbientLifecycleObserverInterface {
+ ctor public AmbientLifecycleObserver(android.app.Activity activity, java.util.concurrent.Executor callbackExecutor, androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientLifecycleCallback callbacks);
+ ctor public AmbientLifecycleObserver(android.app.Activity activity, androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientLifecycleCallback callbacks);
+ method public boolean isAmbient();
+ }
+
+ public interface AmbientLifecycleObserverInterface extends androidx.lifecycle.DefaultLifecycleObserver {
+ method public boolean isAmbient();
+ }
+
+ public static final class AmbientLifecycleObserverInterface.AmbientDetails {
+ ctor public AmbientLifecycleObserverInterface.AmbientDetails(boolean burnInProtectionRequired, boolean deviceHasLowBitAmbient);
+ method public boolean getBurnInProtectionRequired();
+ method public boolean getDeviceHasLowBitAmbient();
+ property public final boolean burnInProtectionRequired;
+ property public final boolean deviceHasLowBitAmbient;
+ }
+
+ public static interface AmbientLifecycleObserverInterface.AmbientLifecycleCallback {
+ method public default void onEnterAmbient(androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientDetails ambientDetails);
+ method public default void onExitAmbient();
+ method public default void onUpdateAmbient();
+ }
+
@Deprecated public final class AmbientMode extends android.app.Fragment {
ctor @Deprecated public AmbientMode();
method @Deprecated public static <T extends android.app.Activity> androidx.wear.ambient.AmbientMode.AmbientController! attachAmbientSupport(T!);
@@ -50,30 +74,30 @@
method @Deprecated public void setAmbientOffloadEnabled(boolean);
}
- public final class AmbientModeSupport extends androidx.fragment.app.Fragment {
- ctor public AmbientModeSupport();
- method public static <T extends androidx.fragment.app.FragmentActivity> androidx.wear.ambient.AmbientModeSupport.AmbientController! attach(T!);
- field public static final String EXTRA_BURN_IN_PROTECTION = "com.google.android.wearable.compat.extra.BURN_IN_PROTECTION";
- field public static final String EXTRA_LOWBIT_AMBIENT = "com.google.android.wearable.compat.extra.LOWBIT_AMBIENT";
- field public static final String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode";
+ @Deprecated public final class AmbientModeSupport extends androidx.fragment.app.Fragment {
+ ctor @Deprecated public AmbientModeSupport();
+ method @Deprecated public static <T extends androidx.fragment.app.FragmentActivity> androidx.wear.ambient.AmbientModeSupport.AmbientController! attach(T!);
+ field @Deprecated public static final String EXTRA_BURN_IN_PROTECTION = "com.google.android.wearable.compat.extra.BURN_IN_PROTECTION";
+ field @Deprecated public static final String EXTRA_LOWBIT_AMBIENT = "com.google.android.wearable.compat.extra.LOWBIT_AMBIENT";
+ field @Deprecated public static final String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode";
}
- public abstract static class AmbientModeSupport.AmbientCallback {
- ctor public AmbientModeSupport.AmbientCallback();
- method public void onAmbientOffloadInvalidated();
- method public void onEnterAmbient(android.os.Bundle!);
- method public void onExitAmbient();
- method public void onUpdateAmbient();
+ @Deprecated public abstract static class AmbientModeSupport.AmbientCallback {
+ ctor @Deprecated public AmbientModeSupport.AmbientCallback();
+ method @Deprecated public void onAmbientOffloadInvalidated();
+ method @Deprecated public void onEnterAmbient(android.os.Bundle!);
+ method @Deprecated public void onExitAmbient();
+ method @Deprecated public void onUpdateAmbient();
}
- public static interface AmbientModeSupport.AmbientCallbackProvider {
- method public androidx.wear.ambient.AmbientModeSupport.AmbientCallback! getAmbientCallback();
+ @Deprecated public static interface AmbientModeSupport.AmbientCallbackProvider {
+ method @Deprecated public androidx.wear.ambient.AmbientModeSupport.AmbientCallback! getAmbientCallback();
}
- public final class AmbientModeSupport.AmbientController {
- method public boolean isAmbient();
- method public void setAmbientOffloadEnabled(boolean);
- method public void setAutoResumeEnabled(boolean);
+ @Deprecated public final class AmbientModeSupport.AmbientController {
+ method @Deprecated public boolean isAmbient();
+ method @Deprecated public void setAmbientOffloadEnabled(boolean);
+ method @Deprecated public void setAutoResumeEnabled(boolean);
}
}
diff --git a/wear/wear/api/restricted_current.txt b/wear/wear/api/restricted_current.txt
index 38baf3e1..023be62 100644
--- a/wear/wear/api/restricted_current.txt
+++ b/wear/wear/api/restricted_current.txt
@@ -17,6 +17,30 @@
package androidx.wear.ambient {
+ public final class AmbientLifecycleObserver implements androidx.wear.ambient.AmbientLifecycleObserverInterface {
+ ctor public AmbientLifecycleObserver(android.app.Activity activity, java.util.concurrent.Executor callbackExecutor, androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientLifecycleCallback callbacks);
+ ctor public AmbientLifecycleObserver(android.app.Activity activity, androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientLifecycleCallback callbacks);
+ method public boolean isAmbient();
+ }
+
+ public interface AmbientLifecycleObserverInterface extends androidx.lifecycle.DefaultLifecycleObserver {
+ method public boolean isAmbient();
+ }
+
+ public static final class AmbientLifecycleObserverInterface.AmbientDetails {
+ ctor public AmbientLifecycleObserverInterface.AmbientDetails(boolean burnInProtectionRequired, boolean deviceHasLowBitAmbient);
+ method public boolean getBurnInProtectionRequired();
+ method public boolean getDeviceHasLowBitAmbient();
+ property public final boolean burnInProtectionRequired;
+ property public final boolean deviceHasLowBitAmbient;
+ }
+
+ public static interface AmbientLifecycleObserverInterface.AmbientLifecycleCallback {
+ method public default void onEnterAmbient(androidx.wear.ambient.AmbientLifecycleObserverInterface.AmbientDetails ambientDetails);
+ method public default void onExitAmbient();
+ method public default void onUpdateAmbient();
+ }
+
@Deprecated public final class AmbientMode extends android.app.Fragment {
ctor @Deprecated public AmbientMode();
method @Deprecated public static <T extends android.app.Activity> androidx.wear.ambient.AmbientMode.AmbientController! attachAmbientSupport(T!);
@@ -50,30 +74,30 @@
method @Deprecated public void setAmbientOffloadEnabled(boolean);
}
- public final class AmbientModeSupport extends androidx.fragment.app.Fragment {
- ctor public AmbientModeSupport();
- method public static <T extends androidx.fragment.app.FragmentActivity> androidx.wear.ambient.AmbientModeSupport.AmbientController! attach(T!);
- field public static final String EXTRA_BURN_IN_PROTECTION = "com.google.android.wearable.compat.extra.BURN_IN_PROTECTION";
- field public static final String EXTRA_LOWBIT_AMBIENT = "com.google.android.wearable.compat.extra.LOWBIT_AMBIENT";
- field public static final String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode";
+ @Deprecated public final class AmbientModeSupport extends androidx.fragment.app.Fragment {
+ ctor @Deprecated public AmbientModeSupport();
+ method @Deprecated public static <T extends androidx.fragment.app.FragmentActivity> androidx.wear.ambient.AmbientModeSupport.AmbientController! attach(T!);
+ field @Deprecated public static final String EXTRA_BURN_IN_PROTECTION = "com.google.android.wearable.compat.extra.BURN_IN_PROTECTION";
+ field @Deprecated public static final String EXTRA_LOWBIT_AMBIENT = "com.google.android.wearable.compat.extra.LOWBIT_AMBIENT";
+ field @Deprecated public static final String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode";
}
- public abstract static class AmbientModeSupport.AmbientCallback {
- ctor public AmbientModeSupport.AmbientCallback();
- method public void onAmbientOffloadInvalidated();
- method public void onEnterAmbient(android.os.Bundle!);
- method public void onExitAmbient();
- method public void onUpdateAmbient();
+ @Deprecated public abstract static class AmbientModeSupport.AmbientCallback {
+ ctor @Deprecated public AmbientModeSupport.AmbientCallback();
+ method @Deprecated public void onAmbientOffloadInvalidated();
+ method @Deprecated public void onEnterAmbient(android.os.Bundle!);
+ method @Deprecated public void onExitAmbient();
+ method @Deprecated public void onUpdateAmbient();
}
- public static interface AmbientModeSupport.AmbientCallbackProvider {
- method public androidx.wear.ambient.AmbientModeSupport.AmbientCallback! getAmbientCallback();
+ @Deprecated public static interface AmbientModeSupport.AmbientCallbackProvider {
+ method @Deprecated public androidx.wear.ambient.AmbientModeSupport.AmbientCallback! getAmbientCallback();
}
- public final class AmbientModeSupport.AmbientController {
- method public boolean isAmbient();
- method public void setAmbientOffloadEnabled(boolean);
- method public void setAutoResumeEnabled(boolean);
+ @Deprecated public final class AmbientModeSupport.AmbientController {
+ method @Deprecated public boolean isAmbient();
+ method @Deprecated public void setAmbientOffloadEnabled(boolean);
+ method @Deprecated public void setAutoResumeEnabled(boolean);
}
}
diff --git a/wear/wear/build.gradle b/wear/wear/build.gradle
index a85136e..83bcb50 100644
--- a/wear/wear/build.gradle
+++ b/wear/wear/build.gradle
@@ -14,6 +14,7 @@
api("androidx.core:core:1.6.0")
api("androidx.versionedparcelable:versionedparcelable:1.1.1")
api('androidx.dynamicanimation:dynamicanimation:1.0.0')
+ api('androidx.lifecycle:lifecycle-runtime:2.5.1')
androidTestImplementation(project(":test:screenshot:screenshot"))
androidTestImplementation(libs.kotlinStdlib)
diff --git a/wear/wear/src/main/java/androidx/wear/ambient/AmbientLifecycleObserver.kt b/wear/wear/src/main/java/androidx/wear/ambient/AmbientLifecycleObserver.kt
new file mode 100644
index 0000000..7bafa39
--- /dev/null
+++ b/wear/wear/src/main/java/androidx/wear/ambient/AmbientLifecycleObserver.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.wear.ambient
+
+import android.app.Activity
+import android.os.Bundle
+import androidx.lifecycle.LifecycleOwner
+import com.google.android.wearable.compat.WearableActivityController
+import java.util.concurrent.Executor
+
+/**
+ * Lifecycle Observer which can be used to add ambient support to an activity on Wearable devices.
+ *
+ * Applications which wish to show layouts in ambient mode should attach this observer to their
+ * activities or fragments, passing in a set of callback to be notified about ambient state. In
+ * addition, the app needs to declare that it uses the [android.Manifest.permission.WAKE_LOCK]
+ * permission in its manifest.
+ *
+ * The created [AmbientLifecycleObserver] can also be used to query whether the device is in
+ * ambient mode.
+ *
+ * As an example of how to use this class, see the following example:
+ *
+ * ```
+ * class MyActivity : ComponentActivity() {
+ * private val callbacks = object : AmbientLifecycleObserver.AmbientLifecycleCallback {
+ * // ...
+ * }
+ *
+ * private val ambientObserver = DefaultAmbientLifecycleObserver(this, callbacks)
+ *
+ * override fun onCreate(savedInstanceState: Bundle) {
+ * lifecycle.addObserver(ambientObserver)
+ * }
+ * }
+ * ```
+ *
+ * @param activity The activity that this observer is being attached to.
+ * @param callbackExecutor The executor to run the provided callbacks on.
+ * @param callbacks An instance of [AmbientLifecycleObserverInterface.AmbientLifecycleCallback], used to
+ * notify the observer about changes to the ambient state.
+ */
+@Suppress("CallbackName")
+class AmbientLifecycleObserver(
+ activity: Activity,
+ callbackExecutor: Executor,
+ callbacks: AmbientLifecycleObserverInterface.AmbientLifecycleCallback,
+) : AmbientLifecycleObserverInterface {
+ private val delegate: AmbientDelegate
+ private val callbackTranslator = object : AmbientDelegate.AmbientCallback {
+ override fun onEnterAmbient(ambientDetails: Bundle?) {
+ val burnInProtection = ambientDetails?.getBoolean(
+ WearableActivityController.EXTRA_BURN_IN_PROTECTION) ?: false
+ val lowBitAmbient = ambientDetails?.getBoolean(
+ WearableActivityController.EXTRA_LOWBIT_AMBIENT) ?: false
+ callbackExecutor.run {
+ callbacks.onEnterAmbient(AmbientLifecycleObserverInterface.AmbientDetails(
+ burnInProtectionRequired = burnInProtection,
+ deviceHasLowBitAmbient = lowBitAmbient
+ ))
+ }
+ }
+
+ override fun onUpdateAmbient() {
+ callbackExecutor.run { callbacks.onUpdateAmbient() }
+ }
+
+ override fun onExitAmbient() {
+ callbackExecutor.run { callbacks.onExitAmbient() }
+ }
+
+ override fun onAmbientOffloadInvalidated() {
+ }
+ }
+
+ /**
+ * Construct a [AmbientLifecycleObserver], using the UI thread to dispatch ambient
+ * callbacks.
+ *
+ * @param activity The activity that this observer is being attached to.
+ * @param callbacks An instance of [AmbientLifecycleObserverInterface.AmbientLifecycleCallback], used to
+ * notify the observer about changes to the ambient state.
+ */
+ constructor(
+ activity: Activity,
+ callbacks: AmbientLifecycleObserverInterface.AmbientLifecycleCallback
+ ) : this(activity, { r -> r.run() }, callbacks)
+
+ init {
+ delegate = AmbientDelegate(activity, WearableControllerProvider(), callbackTranslator)
+ }
+
+ override fun isAmbient(): Boolean = delegate.isAmbient
+
+ override fun onCreate(owner: LifecycleOwner) {
+ super.onCreate(owner)
+ delegate.onCreate()
+ delegate.setAmbientEnabled()
+ }
+
+ override fun onResume(owner: LifecycleOwner) {
+ super.onResume(owner)
+ delegate.onResume()
+ }
+
+ override fun onPause(owner: LifecycleOwner) {
+ super.onPause(owner)
+ delegate.onPause()
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ super.onStop(owner)
+ delegate.onStop()
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ super.onDestroy(owner)
+ delegate.onDestroy()
+ }
+}
\ No newline at end of file
diff --git a/wear/wear/src/main/java/androidx/wear/ambient/AmbientLifecycleObserverInterface.kt b/wear/wear/src/main/java/androidx/wear/ambient/AmbientLifecycleObserverInterface.kt
new file mode 100644
index 0000000..353a695
--- /dev/null
+++ b/wear/wear/src/main/java/androidx/wear/ambient/AmbientLifecycleObserverInterface.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.wear.ambient
+
+import androidx.lifecycle.DefaultLifecycleObserver
+
+/**
+ * Interface for LifecycleObservers which are used to add ambient mode support to activities on
+ * Wearable devices.
+ *
+ * This interface can be implemented, or faked out, to allow for testing activities which use
+ * ambient support. Applications should use [AmbientLifecycleObserver] to implement ambient support
+ * on real devices.
+ */
+@Suppress("CallbackName")
+interface AmbientLifecycleObserverInterface : DefaultLifecycleObserver {
+ /**
+ * Details about ambient mode support on the current device, passed to
+ * [AmbientLifecycleCallback.onEnterAmbient].
+ *
+ * @param burnInProtectionRequired whether the ambient layout must implement burn-in protection.
+ * When this property is set to true, views must be shifted around periodically in ambient
+ * mode. To ensure that content isn't shifted off the screen, avoid placing content within
+ * 10 pixels of the edge of the screen. Activities should also avoid solid white areas to
+ * prevent pixel burn-in. Both of these requirements only apply in ambient mode, and only
+ * when this property is set to true.
+ * @param deviceHasLowBitAmbient whether this device has low-bit ambient mode. When this
+ * property is set to true, the screen supports fewer bits for each color in ambient mode.
+ * In this case, activities should disable anti-aliasing in ambient mode.
+ */
+ class AmbientDetails(
+ val burnInProtectionRequired: Boolean,
+ val deviceHasLowBitAmbient: Boolean
+ ) {
+ override fun toString(): String =
+ "AmbientDetails - burnInProtectionRequired: $burnInProtectionRequired, " +
+ "deviceHasLowBitAmbient: $deviceHasLowBitAmbient"
+ }
+
+ /** Callback to receive ambient mode state changes. */
+ interface AmbientLifecycleCallback {
+ /**
+ * Called when an activity is entering ambient mode. This event is sent while an activity is
+ * running (after onResume, before onPause). All drawing should complete by the conclusion
+ * of this method. Note that {@code invalidate()} calls will be executed before resuming
+ * lower-power mode.
+ *
+ * @param ambientDetails instance of [AmbientDetails] containing information about the
+ * display being used.
+ */
+ fun onEnterAmbient(ambientDetails: AmbientDetails) {}
+
+ /**
+ * Called when the system is updating the display for ambient mode. Activities may use this
+ * opportunity to update or invalidate views.
+ */
+ fun onUpdateAmbient() {}
+
+ /**
+ * Called when an activity should exit ambient mode. This event is sent while an activity is
+ * running (after onResume, before onPause).
+ */
+ fun onExitAmbient() {}
+ }
+
+ /**
+ * @return {@code true} if the activity is currently in ambient.
+ */
+ fun isAmbient(): Boolean
+}
diff --git a/wear/wear/src/main/java/androidx/wear/ambient/AmbientModeSupport.java b/wear/wear/src/main/java/androidx/wear/ambient/AmbientModeSupport.java
index 0b4b539..80dcd2f 100644
--- a/wear/wear/src/main/java/androidx/wear/ambient/AmbientModeSupport.java
+++ b/wear/wear/src/main/java/androidx/wear/ambient/AmbientModeSupport.java
@@ -77,7 +77,12 @@
* }
* }
* }</pre>
+ *
+ * @deprecated Use {@link AmbientLifecycleObserverInterface} and {@link AmbientLifecycleObserver}
+ * instead. These classes use lifecycle components instead, preventing the need to hook these
+ * events using fragments.
*/
+@Deprecated
public final class AmbientModeSupport extends Fragment {
private static final String TAG = "AmbientModeSupport";
diff --git a/wear/wear/src/test/java/androidx/wear/ambient/AmbientLifecycleObserverTest.kt b/wear/wear/src/test/java/androidx/wear/ambient/AmbientLifecycleObserverTest.kt
new file mode 100644
index 0000000..d0fee22
--- /dev/null
+++ b/wear/wear/src/test/java/androidx/wear/ambient/AmbientLifecycleObserverTest.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.wear.ambient
+
+import android.os.Bundle
+import androidx.lifecycle.Lifecycle
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.wearable.compat.WearableActivityController
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AmbientLifecycleObserverTest {
+ private lateinit var scenario: ActivityScenario<AmbientLifecycleObserverTestActivity>
+
+ @Before
+ fun setUp() {
+ scenario = AmbientTestActivityUtil.launchActivity(
+ AmbientLifecycleObserverTestActivity::class.java)
+ }
+
+ private fun resetState(controller: WearableActivityController) {
+ controller.mCreateCalled = false
+ controller.mDestroyCalled = false
+ controller.mPauseCalled = false
+ controller.mResumeCalled = false
+ controller.mStopCalled = false
+ }
+
+ @Test
+ fun testEnterAmbientCallback() {
+ scenario.onActivity { activity ->
+ WearableActivityController.getLastInstance().enterAmbient()
+ assertTrue(activity.enterAmbientCalled)
+ assertFalse(activity.exitAmbientCalled)
+ assertFalse(activity.updateAmbientCalled)
+
+ assertNotNull(activity.enterAmbientArgs)
+
+ // Nothing in the bundle, both should be false.
+ assertFalse(activity.enterAmbientArgs!!.burnInProtectionRequired)
+ assertFalse(activity.enterAmbientArgs!!.deviceHasLowBitAmbient)
+ }
+ }
+
+ @Test
+ fun testEnterAmbientCallbackWithArgs() {
+ scenario.onActivity { activity ->
+ val bundle = Bundle()
+ bundle.putBoolean(WearableActivityController.EXTRA_LOWBIT_AMBIENT, true)
+ bundle.putBoolean(WearableActivityController.EXTRA_BURN_IN_PROTECTION, true)
+
+ WearableActivityController.getLastInstance().enterAmbient(bundle)
+
+ assertTrue(activity.enterAmbientArgs!!.burnInProtectionRequired)
+ assertTrue(activity.enterAmbientArgs!!.deviceHasLowBitAmbient)
+ }
+ }
+
+ @Test
+ fun testExitAmbientCallback() {
+ scenario.onActivity { activity ->
+ WearableActivityController.getLastInstance().exitAmbient()
+ assertFalse(activity.enterAmbientCalled)
+ assertTrue(activity.exitAmbientCalled)
+ assertFalse(activity.updateAmbientCalled)
+ }
+ }
+
+ @Test
+ fun testUpdateAmbientCallback() {
+ scenario.onActivity { activity ->
+ WearableActivityController.getLastInstance().updateAmbient()
+ assertFalse(activity.enterAmbientCalled)
+ assertFalse(activity.exitAmbientCalled)
+ assertTrue(activity.updateAmbientCalled)
+ }
+ }
+
+ @Test
+ fun onCreateCanPassThrough() {
+ // Default after launch is that the activity is running.
+ val controller = WearableActivityController.getLastInstance()
+ assertTrue(controller.mCreateCalled)
+ assertFalse(controller.mDestroyCalled)
+ assertFalse(controller.mPauseCalled)
+ assertTrue(controller.mResumeCalled)
+ assertFalse(controller.mStopCalled)
+ }
+
+ @Test
+ fun onPauseCanPassThrough() {
+ val controller = WearableActivityController.getLastInstance()
+ resetState(controller)
+
+ // Note: STARTED is when the app is paused; RUNNING is when it's actually running.
+ scenario.moveToState(Lifecycle.State.STARTED)
+
+ assertFalse(controller.mCreateCalled)
+ assertFalse(controller.mDestroyCalled)
+ assertTrue(controller.mPauseCalled)
+ assertFalse(controller.mResumeCalled)
+ assertFalse(controller.mStopCalled)
+ }
+
+ @Test
+ fun onStopCanPassThrough() {
+ val controller = WearableActivityController.getLastInstance()
+ resetState(controller)
+
+ scenario.moveToState(Lifecycle.State.CREATED)
+
+ assertFalse(controller.mCreateCalled)
+ assertFalse(controller.mDestroyCalled)
+ assertTrue(controller.mPauseCalled)
+ assertFalse(controller.mResumeCalled)
+ assertTrue(controller.mStopCalled)
+ }
+
+ @Test
+ fun onDestroyCanPassThrough() {
+ val controller = WearableActivityController.getLastInstance()
+ resetState(controller)
+
+ scenario.moveToState(Lifecycle.State.DESTROYED)
+
+ assertFalse(controller.mCreateCalled)
+ assertTrue(controller.mDestroyCalled)
+ assertTrue(controller.mPauseCalled)
+ assertFalse(controller.mResumeCalled)
+ assertTrue(controller.mStopCalled)
+ }
+
+ @Test
+ fun canQueryInAmbient() {
+ scenario.onActivity { activity ->
+ val controller = WearableActivityController.getLastInstance()
+ assertFalse(activity.observer.isAmbient())
+ controller.isAmbient = true
+ assertTrue(activity.observer.isAmbient())
+ }
+ }
+}
diff --git a/wear/wear/src/test/java/androidx/wear/ambient/AmbientLifecycleObserverTestActivity.kt b/wear/wear/src/test/java/androidx/wear/ambient/AmbientLifecycleObserverTestActivity.kt
new file mode 100644
index 0000000..508c401
--- /dev/null
+++ b/wear/wear/src/test/java/androidx/wear/ambient/AmbientLifecycleObserverTestActivity.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.ambient
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+
+class AmbientLifecycleObserverTestActivity : ComponentActivity() {
+ private val callback = object : AmbientLifecycleObserverInterface.AmbientLifecycleCallback {
+ override fun onEnterAmbient(
+ ambientDetails: AmbientLifecycleObserverInterface.AmbientDetails
+ ) {
+ enterAmbientCalled = true
+ enterAmbientArgs = ambientDetails
+ }
+
+ override fun onUpdateAmbient() {
+ updateAmbientCalled = true
+ }
+
+ override fun onExitAmbient() {
+ exitAmbientCalled = true
+ }
+ }
+
+ val observer = AmbientLifecycleObserver(this, { r -> r.run() }, callback)
+
+ var enterAmbientCalled = false
+ var enterAmbientArgs: AmbientLifecycleObserverInterface.AmbientDetails? = null
+ var updateAmbientCalled = false
+ var exitAmbientCalled = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ lifecycle.addObserver(observer)
+ }
+}
\ No newline at end of file
diff --git a/wear/wear/src/test/java/com/google/android/wearable/compat/WearableActivityController.java b/wear/wear/src/test/java/com/google/android/wearable/compat/WearableActivityController.java
index eb4d559..79f2291 100644
--- a/wear/wear/src/test/java/com/google/android/wearable/compat/WearableActivityController.java
+++ b/wear/wear/src/test/java/com/google/android/wearable/compat/WearableActivityController.java
@@ -19,11 +19,17 @@
import android.app.Activity;
import android.os.Bundle;
+import androidx.annotation.Nullable;
+
/**
* Mock version of {@link WearableActivityController}. During instrumentation testing, the tests
* would end up using this instead of the version implemented on device.
*/
public class WearableActivityController {
+ public static final java.lang.String EXTRA_BURN_IN_PROTECTION =
+ "com.google.android.wearable.compat.extra.BURN_IN_PROTECTION";
+ public static final java.lang.String EXTRA_LOWBIT_AMBIENT =
+ "com.google.android.wearable.compat.extra.LOWBIT_AMBIENT";
private static WearableActivityController sLastInstance;
@@ -37,20 +43,44 @@
private boolean mAmbient = false;
private boolean mAmbientOffloadEnabled = false;
+ public boolean mCreateCalled = false;
+ public boolean mResumeCalled = false;
+ public boolean mPauseCalled = false;
+ public boolean mStopCalled = false;
+ public boolean mDestroyCalled = false;
+
public WearableActivityController(String tag, Activity activity, AmbientCallback callback) {
sLastInstance = this;
mCallback = callback;
}
// Methods required by the stub but not currently used in tests.
- public void onCreate() {}
- public void onResume() {}
- public void onPause() {}
- public void onStop() {}
- public void onDestroy() {}
+ public void onCreate() {
+ mCreateCalled = true;
+ }
+
+ public void onResume() {
+ mResumeCalled = true;
+ }
+
+ public void onPause() {
+ mPauseCalled = true;
+ }
+
+ public void onStop() {
+ mStopCalled = true;
+ }
+
+ public void onDestroy() {
+ mDestroyCalled = true;
+ }
public void enterAmbient() {
- mCallback.onEnterAmbient(null);
+ enterAmbient(null);
+ }
+
+ public void enterAmbient(@Nullable Bundle enterAmbientArgs) {
+ mCallback.onEnterAmbient(enterAmbientArgs);
}
public void exitAmbient() {
diff --git a/webkit/integration-tests/testapp/build.gradle b/webkit/integration-tests/testapp/build.gradle
index 2d2e298..c7f39ec 100644
--- a/webkit/integration-tests/testapp/build.gradle
+++ b/webkit/integration-tests/testapp/build.gradle
@@ -31,6 +31,7 @@
implementation(libs.espressoIdlingNet)
implementation(libs.espressoIdlingResource)
+ androidTestImplementation(libs.espressoRemote)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
diff --git a/webkit/integration-tests/testapp/src/androidTest/AndroidManifest.xml b/webkit/integration-tests/testapp/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..5ec74fb
--- /dev/null
+++ b/webkit/integration-tests/testapp/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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"
+ xmlns:tools="http://schemas.android.com/tools">
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.example.androidx.webkit"
+ android:targetProcesses="*">
+ <!-- This enables Multiprocess Espresso. -->
+ <meta-data
+ android:name="remoteMethod"
+ android:value="androidx.test.espresso.remote.EspressoRemote#remoteInit" />
+ </instrumentation>
+</manifest>
\ No newline at end of file
diff --git a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java
index 1fdb830..43e3a0d 100644
--- a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java
+++ b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java
@@ -16,6 +16,11 @@
package com.example.androidx.webkit;
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
import static org.junit.Assert.assertTrue;
import androidx.core.content.ContextCompat;
@@ -52,15 +57,32 @@
@Test
public void testSetDataDirectorySuffix() throws Throwable {
final String dataDirPrefix = "app_webview_";
- final String dataDirSuffix = "per_process_webview_data_0";
-
- WebkitTestHelpers.clickMenuListItemWithString(
- R.string.process_global_config_activity_title);
- Thread.sleep(1000);
-
+ final String dataDirSuffix = "per_process_webview_data_test";
File file = new File(ContextCompat.getDataDir(ApplicationProvider.getApplicationContext()),
dataDirPrefix + dataDirSuffix);
+ // Ensures WebView directory created during earlier tests runs are purged.
+ deleteDirectory(file);
+ // This should ideally be an assumption, but we want a stronger signal to ensure the test
+ // does not silently stop working.
+ if (file.exists()) {
+ throw new RuntimeException("WebView Directory exists before test despite attempt to "
+ + "delete it");
+ }
+ WebkitTestHelpers.clickMenuListItemWithString(
+ R.string.process_global_config_activity_title);
+ onView(withId(R.id.process_global_textview)).check(matches(withText("WebView Loaded!")));
+
assertTrue(file.exists());
}
+
+ private static boolean deleteDirectory(File directoryToBeDeleted) {
+ File[] allContents = directoryToBeDeleted.listFiles();
+ if (allContents != null) {
+ for (File file : allContents) {
+ deleteDirectory(file);
+ }
+ }
+ return directoryToBeDeleted.delete();
+ }
}
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
index acad3fa..b6fb511 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
@@ -20,6 +20,7 @@
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;
+import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
@@ -45,11 +46,14 @@
}
ProcessGlobalConfig config = new ProcessGlobalConfig();
config.setDataDirectorySuffix(this,
- "per_process_webview_data_0");
+ "per_process_webview_data_test");
ProcessGlobalConfig.apply(config);
setContentView(R.layout.activity_process_global_config);
WebView wv = findViewById(R.id.process_global_config_webview);
+ wv.getSettings().setJavaScriptEnabled(true);
wv.setWebViewClient(new WebViewClient());
wv.loadUrl("www.google.com");
+ TextView tv = findViewById(R.id.process_global_textview);
+ tv.setText("WebView Loaded!");
}
}
diff --git a/webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml b/webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml
index c2717b2..e898b1d 100644
--- a/webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml
+++ b/webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml
@@ -14,8 +14,25 @@
limitations under the License.
-->
-<WebView
-xmlns:android="http://schemas.android.com/apk/res/android"
-android:id="@+id/process_global_config_webview"
-android:layout_width="match_parent"
-android:layout_height="match_parent" />
\ No newline at end of file
+<!--Orientation is decided at runtime-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/activity_medium_interstitial"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:ignore="Orientation">
+
+ <TextView
+ android:id="@+id/process_global_textview"
+ android:layout_width="wrap_content"
+ android:layout_height="105dp"
+ android:text="TextView" />
+
+ <WebView
+ android:id="@+id/process_global_config_webview"
+ android:layout_width="match_parent"
+ android:layout_height="604dp"></WebView>
+</LinearLayout>
+
+
diff --git a/work/OWNERS b/work/OWNERS
index e994ca6..d390fd5 100644
--- a/work/OWNERS
+++ b/work/OWNERS
@@ -1,6 +1,7 @@
-# Bug component: 409906
+# Bug component: 324783
+sergeyv@google.com
sumir@google.com
rahulrav@google.com
ilake@google.com
-per-file settings.gradle = dustinlam@google.com, rahulrav@google.com
+per-file settings.gradle = dustinlam@google.com, rahulrav@google.com, sergeyv@google.com
diff --git a/work/work-runtime/src/androidTest/AndroidManifest.xml b/work/work-runtime/src/androidTest/AndroidManifest.xml
index 3f54449..e636ee9 100644
--- a/work/work-runtime/src/androidTest/AndroidManifest.xml
+++ b/work/work-runtime/src/androidTest/AndroidManifest.xml
@@ -17,10 +17,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!--
- ~ Adding this permission to the test-app's AndroidManifest.xml. This is because
+ ~ Adding these permissions to the test-app's AndroidManifest.xml. This is because
~ we don't want applications to implicitly add this permission on API 31 and above. We only
~ need this permission for tests.
-->
- <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
+ <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
+ android:maxSdkVersion="32"/>
+ <uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
<application android:name="androidx.multidex.MultiDexApplication"/>
</manifest>
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
index 0354b64..2fcecaa3 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
@@ -22,6 +22,7 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import androidx.test.filters.RequiresDevice
import androidx.test.filters.SdkSuppress
import androidx.work.WorkInfo.State
import androidx.work.WorkManager.UpdateResult.APPLIED_FOR_NEXT_RUN
@@ -215,6 +216,7 @@
workManager.awaitSuccess(requestId)
}
+ @RequiresDevice // b/266498479
@Test
@MediumTest
fun progressReset() {
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/AlarmsTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/AlarmsTest.java
index 2c2aced..164a3ef 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/AlarmsTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/AlarmsTest.java
@@ -25,11 +25,9 @@
import static org.hamcrest.MatcherAssert.assertThat;
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.work.DatabaseTest;
import androidx.work.OneTimeWorkRequest;
@@ -51,12 +49,7 @@
private final long mTriggerAt = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1);
@Test
- @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
public void testSetAlarm_noPreExistingAlarms() {
- if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
- return; // b/262909049: Do not run this test on pre-release Android U.
- }
-
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class).build();
insertWork(work);
WorkGenerationalId workSpecId = generationalId(work.getWorkSpec());
@@ -67,12 +60,7 @@
}
@Test
- @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
public void testSetAlarm_withPreExistingAlarms() {
- if (Build.VERSION.SDK_INT == 33 && !"REL".equals(Build.VERSION.CODENAME)) {
- return; // b/262909049: Do not run this test on pre-release Android U.
- }
-
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class).build();
insertWork(work);
WorkGenerationalId workSpecId = generationalId(work.getWorkSpec());
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
index bd85def..d0f484a 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
@@ -788,6 +788,10 @@
public void setReschedulePendingResult(
@NonNull BroadcastReceiver.PendingResult rescheduleReceiverResult) {
synchronized (sLock) {
+ // if we have two broadcast in the row, finish old one and use new one
+ if (mRescheduleReceiverResult != null) {
+ mRescheduleReceiverResult.finish();
+ }
mRescheduleReceiverResult = rescheduleReceiverResult;
if (mForceStopRunnableCompleted) {
mRescheduleReceiverResult.finish();